chore(deps): make Qt backend optional (#350)

* chore(deps): make Qt backend optional

TODO:
- [ ] Add relevant entry in CHANGELOG
- [ ] Update install documentation
- [ ] Make sure `manim-slides convert` can run without any Qt backend
- [ ] Make sure test suite works (partially) without any Qt backend
- [ ] Make sure we can import `manim_slides` without any Qt backend

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

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

* chore(deps): some fixes but wip

* chore(docs): update

* chore(deps): support PyQt6

* chore(deps): make Qt backend optional

TODO:
- [ ] Add relevant entry in CHANGELOG
- [ ] Update install documentation
- [ ] Make sure `manim-slides convert` can run without any Qt backend
- [ ] Make sure test suite works (partially) without any Qt backend
- [ ] Make sure we can import `manim_slides` without any Qt backend

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

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

* chore(deps): some fixes but wip

* chore(docs): update

* chore(deps): support PyQt6

* fix(deps): ci and docs

* fix(lib): missing package

* chore(ci): does it work?

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

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

* chore(test): skip failing

* chore(docs): update

* chore(docs): update

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

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

* fix(docs): typo

* fix(test): quit instead of shutdown

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jérome Eertmans
2024-01-26 15:08:23 +01:00
committed by GitHub
parent f260d0d310
commit 16f740d2ad
21 changed files with 926 additions and 813 deletions

View File

@ -52,7 +52,7 @@ jobs:
uses: nikeee/setup-pandoc@v1
- name: Install local Python package
run: pdm install -dGdocs -dGgithub-action
run: pdm sync -Gdocs -Ggithub-action
- name: Install IPython kernel
run: pdm run ipython kernel install --name "manim-slides" --user

View File

@ -13,7 +13,7 @@ jobs:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
pyversion: ['3.8', '3.9', '3.10', '3.11']
pyversion: ['3.9', '3.10', '3.11', '3.12']
runs-on: ${{ matrix.os }}
env:
QT_QPA_PLATFORM: offscreen
@ -69,7 +69,7 @@ jobs:
- name: Install Manim Slides
run: |
pdm install -dGgithub-action -dGtest
pdm sync -Ggithub-action -Gtest
- name: Run pytest
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'

View File

@ -52,6 +52,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#335](https://github.com/jeertmans/manim-slides/pull/335)
- Changed build backend to PDM and reflected on docs.
[#354](https://github.com/jeertmans/manim-slides/pull/354)
- Dropped Python 3.8 support.
[#350](https://github.com/jeertmans/manim-slides/pull/350)
- Made Qt backend optional and support PyQt6 too.
[#350](https://github.com/jeertmans/manim-slides/pull/350)
- Documentated how to create and use a custom HTML template.
[#357](https://github.com/jeertmans/manim-slides/pull/357)

View File

@ -88,11 +88,15 @@ manim-slides render example.py BasicExample
# or use ManimGL
manim-slides render --GL example.py BasicExample
```
<!-- end usage -->
> [!NOTE]
> Using `manim-slides render` makes sure the use the `manim`
> (or `manimlib`) library that was installed in the Python same environment.
> Put simply, this is a wrapper of `manim render [ARGS]...` (or `manimgl [ARGS]...`).
> Using `manim-slides render` makes sure to use the `manim`
> (or `manimlib`) library that was installed in the same Python environment.
> Put simply, this is a wrapper around
> `manim render [ARGS]...` (or `manimgl [ARGS]...`).
<!-- start more-usage -->
To start the presentation using `Scene1`, `Scene2` and so on, run:
@ -106,7 +110,7 @@ In our example:
manim-slides BasicExample
```
<!-- end usage -->
<!-- end more-usage -->
<p align="center">
<img alt="Example GIF" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/example.gif">

View File

@ -48,11 +48,11 @@ pdm install -Gmanimgl # For ManimGL
Additionnally, Manim Slides comes with groups of dependencies for development purposes:
```bash
pdm install -dGdev # For linters and formatters
pdm install -Gdev # For linters and formatters
# or
pdm install --dGdocs # To build the documentation locally
pdm install -Gdocs # To build the documentation locally
# or
pdm install --dGtests # To run tests
pdm install -Gtest # To run tests
```
:::{note}

View File

@ -34,16 +34,23 @@ 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
The recommended way to install the latest release is to use pip:
The recommended way to install the latest release
with all features is to use pipx:
```bash
pipx install -U manim-slides
pipx install -U "manim-slides[pyside6-full]"
```
:::{tip}
@ -52,6 +59,28 @@ like to upgrade to the latest version available,
if Manim Slides is already installed.
:::
:::{note}
The quotes `"` are added because not all shell support unquoted
brackets (e.g., zsh) or commas (e.g., Windows).
:::
You can check that Manim Slides was correctly installed with:
```bash
manim-slides --version
```
## Custom install
If you want more control on what dependencies are installed,
you can always install the bare minimal dependencies with:
```bash
pipx install -U manim-slides
```
And install additional dependencies later.
Optionally, you can also install Manim or ManimGL using extras[^1]:
```bash
@ -60,11 +89,8 @@ pipx install -U "manim-slides[manim]" # For Manim
pipx install -U "manim-slides[manimgl]" # For ManimGL
```
You can check that Manim Slides was correctly installed with:
```bash
manim-slides --version
```
For optional dependencies documentation, see
[next section](#optional-dependencies).
:::{warning}
If you are installing with pipx, this is mandatory to at least include
@ -74,20 +100,30 @@ either `manim` or `manimgl`.
[^1]: You still need to have Manim or ManimGL platform-specific dependencies
installed on your computer.
## Optional Dependencies
## Optional dependencies
Along with the optional dependencies for Manim and ManimGL,
Manim Slides offers additional *extras*, that can be activated
using optional dependencies:
- `full`, to include `magic`, `manim`, `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-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];
- `pyside6-full` to include `full` and `pyside6`;
- `sphinx-directive`, to generate presentation inside your Sphinx
documentation. This automatically installs `manim`,
and does not work with ManimGL;
and does not work with ManimGL.
Installing those extras can be done with the following syntax:
@ -95,14 +131,27 @@ Installing those extras can be done with the following syntax:
pipx install -U "manim-slides[extra1,extra2]"
```
:::{note}
The quotes `"` are added because not all shell support unquoted
brackets (e.g., zsh) or commas (e.g., Windows).
:::
[^2]: Actually, PySide6 can be installed on Python 3.12, but you will then
observe the same visual bug as with PyQt6.
## Install From Repository
## When you need a Qt backend
Before `v5.1`, Manim Slides automatically included PySide6 as
a Qt backend. As only `manim-slides present` and `manim-slides wizard`
command need a graphical library, and installing PySide6 on all platforms
and Python version can be sometimes complicated, Manim Slides chooses
**not to include** any Qt backend.
The use can choose between PySide6 (best) and PyQt6, depending on their
availability and licensing rules.
As of `v5.1`, you **need** to have Qt bindings installed to use
`manim-slides present` or `manim-slides wizard`. The recommended way to
install those are via optional dependencies, as explained above.
## Install from source
An alternative way to install Manim Slides is to clone the git repository,
and install from there: read the
and build the package from source. Read the
[contributing guide](./contributing/workflow)
to know how to process.

View File

@ -10,6 +10,19 @@ see [installation](./installation).
:end-before: <!-- end usage -->
```
:::{note}
Using `manim-slides render` makes sure to use the `manim`
(or `manimlib`) library that was installed in the same Python environment.
Put simply, this is a wrapper around
`manim render [ARGS]...` (or `manimgl [ARGS]...`).
:::
```{include} ../../README.md
:start-after: <!-- start more-usage -->
:end-before: <!-- end more-usage -->
```
The output slides should look this this:
```{eval-rst}

View File

@ -17,7 +17,6 @@ from pydantic import (
model_validator,
)
from pydantic_extra_types.color import Color
from PySide6.QtCore import Qt
from .logger import logger
@ -38,6 +37,13 @@ class Signal(BaseModel): # type: ignore[misc]
receiver(*args)
def key_id(name: str) -> PositiveInt:
"""Avoid importing Qt too early."""
from qtpy.QtCore import Qt
return getattr(Qt, f"Key_{name}")
class Key(BaseModel): # type: ignore[misc]
"""Represents a list of key codes, with optionally a name."""
@ -73,14 +79,22 @@ class Key(BaseModel): # type: ignore[misc]
class Keys(BaseModel): # type: ignore[misc]
QUIT: Key = Key(ids=[Qt.Key_Q], name="QUIT")
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
NEXT: Key = Key(ids=[Qt.Key_Right], name="NEXT")
PREVIOUS: Key = Key(ids=[Qt.Key_Left], name="PREVIOUS")
REVERSE: Key = Key(ids=[Qt.Key_V], name="REVERSE")
REPLAY: Key = Key(ids=[Qt.Key_R], name="REPLAY")
FULL_SCREEN: Key = Key(ids=[Qt.Key_F], name="TOGGLE FULL SCREEN")
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
QUIT: Key = Field(default_factory=lambda: Key(ids=[key_id("Q")], name="QUIT"))
PLAY_PAUSE: Key = Field(
default_factory=lambda: Key(ids=[key_id("Space")], name="PLAY / PAUSE")
)
NEXT: Key = Field(default_factory=lambda: Key(ids=[key_id("Right")], name="NEXT"))
PREVIOUS: Key = Field(
default_factory=lambda: Key(ids=[key_id("Left")], name="PREVIOUS")
)
REVERSE: Key = Field(default_factory=lambda: Key(ids=[key_id("V")], name="REVERSE"))
REPLAY: Key = Field(default_factory=lambda: Key(ids=[key_id("R")], name="REPLAY"))
FULL_SCREEN: Key = Field(
default_factory=lambda: Key(ids=[key_id("F")], name="TOGGLE FULL SCREEN")
)
HIDE_MOUSE: Key = Field(
default_factory=lambda: Key(ids=[key_id("H")], name="HIDE / SHOW MOUSE")
)
@model_validator(mode="before")
@classmethod
@ -121,7 +135,7 @@ class Keys(BaseModel): # type: ignore[misc]
class Config(BaseModel): # type: ignore[misc]
"""General Manim Slides config."""
keys: Keys = Keys()
keys: Keys = Field(default_factory=Keys)
@classmethod
def from_file(cls, path: Path) -> "Config":
@ -326,6 +340,3 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
shutil.copy(rev_file, rev_dest)
return self
DEFAULT_CONFIG = Config()

View File

@ -648,14 +648,16 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"config_options",
multiple=True,
callback=validate_config_option,
help="Configuration options passed to the converter. E.g., pass `-cslide_number=true` to display slide numbers.",
help="Configuration options passed to the converter. "
"E.g., pass ``-cslide_number=true`` to display slide numbers.",
)
@click.option(
"--use-template",
"template",
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`.",
help="Use the template given by FILE instead of default one. "
"To echo the default template, use ``--show-template``.",
)
@show_template_option
@show_config_options

View File

@ -6,18 +6,10 @@ from typing import List, Optional, Tuple
import click
from click import Context, Parameter
from pydantic import ValidationError
from PySide6.QtCore import Qt
from ..commons import config_path_option, folder_path_option, verbosity_option
from ..config import Config, PresentationConfig
from ..logger import logger
from ..qt_utils import qapp
from .player import Player
ASPECT_RATIO_MODES = {
"keep": Qt.KeepAspectRatio,
"ignore": Qt.IgnoreAspectRatio,
}
@click.command()
@ -130,7 +122,8 @@ def start_at_callback(
return tuple(map(str_to_int_or_none, values_tuple))
raise click.BadParameter(
f"exactly 2 arguments are expected but you gave {n_values}, please use commas to separate them",
f"exactly 2 arguments are expected but you gave {n_values}, "
"please use commas to separate them",
ctx=ctx,
param=param,
)
@ -283,6 +276,8 @@ def present(
if start_at[1]:
start_at_slide_number = start_at[1]
from ..qt_utils import qapp
app = qapp()
app.setApplicationName("Manim Slides")
@ -298,6 +293,15 @@ def present(
else:
screen = None
from qtpy.QtCore import Qt
aspect_ratio_modes = {
"keep": Qt.KeepAspectRatio,
"ignore": Qt.IgnoreAspectRatio,
}
from .player import Player
player = Player(
config,
presentation_configs,
@ -306,7 +310,7 @@ def present(
skip_all=skip_all,
exit_after_last_slide=exit_after_last_slide,
hide_mouse=hide_mouse,
aspect_ratio_mode=ASPECT_RATIO_MODES[aspect_ratio],
aspect_ratio_mode=aspect_ratio_modes[aspect_ratio],
presentation_index=start_at_scene_number,
slide_index=start_at_slide_number,
screen=screen,

View File

@ -2,11 +2,11 @@ from datetime import datetime
from pathlib import Path
from typing import List, Optional
from PySide6.QtCore import Qt, QTimer, QUrl, Signal, Slot
from PySide6.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
from PySide6.QtMultimedia import QMediaPlayer
from PySide6.QtMultimediaWidgets import QVideoWidget
from PySide6.QtWidgets import (
from qtpy.QtCore import Qt, QTimer, QUrl, Signal, Slot
from qtpy.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
from qtpy.QtMultimedia import QMediaPlayer
from qtpy.QtMultimediaWidgets import QVideoWidget
from qtpy.QtWidgets import (
QHBoxLayout,
QLabel,
QMainWindow,
@ -271,7 +271,7 @@ class Player(QMainWindow): # type: ignore[misc]
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
self.media_player.setLoops(1) # Otherwise looping slides never end
if status == QMediaPlayer.EndOfMedia:
if status == QMediaPlayer.MediaStatus.EndOfMedia:
self.load_next_slide()
self.media_player.mediaStatusChanged.connect(media_status_changed)
@ -280,7 +280,7 @@ class Player(QMainWindow): # type: ignore[misc]
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
if (
status == QMediaPlayer.EndOfMedia
status == QMediaPlayer.MediaStatus.EndOfMedia
and self.current_slide_config.auto_next
):
self.load_next_slide()
@ -390,7 +390,7 @@ class Player(QMainWindow): # type: ignore[misc]
"""
def load_current_media(self, start_paused: bool = False) -> None:
url = QUrl.fromLocalFile(self.current_file)
url = QUrl.fromLocalFile(str(self.current_file))
self.media_player.setSource(url)
if self.playing_reversed_slide:
@ -475,7 +475,7 @@ class Player(QMainWindow): # type: ignore[misc]
def preview_next_slide(self) -> None:
if slide_config := self.next_slide_config:
url = QUrl.fromLocalFile(slide_config.file)
url = QUrl.fromLocalFile(str(slide_config.file))
self.info.next_media_player.setSource(url)
self.info.next_media_player.play()
@ -493,7 +493,7 @@ class Player(QMainWindow): # type: ignore[misc]
@Slot()
def next(self) -> None:
if self.media_player.playbackState() == QMediaPlayer.PausedState:
if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PausedState:
self.media_player.play()
elif self.next_terminates_loop and self.media_player.loops() != 1:
position = self.media_player.position()
@ -521,9 +521,9 @@ class Player(QMainWindow): # type: ignore[misc]
@Slot()
def play_pause(self) -> None:
state = self.media_player.playbackState()
if state == QMediaPlayer.PausedState:
if state == QMediaPlayer.PlaybackState.PausedState:
self.media_player.play()
elif state == QMediaPlayer.PlayingState:
elif state == QMediaPlayer.PlaybackState.PlayingState:
self.media_player.pause()
@Slot()
@ -540,11 +540,9 @@ class Player(QMainWindow): # type: ignore[misc]
else:
self.setCursor(Qt.BlankCursor)
@Slot()
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
self.close()
@Slot()
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
key = event.key()
self.dispatch(key)

View File

@ -1,6 +1,6 @@
"""Qt utils."""
from PySide6.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication
def qapp() -> QApplication:

View File

@ -4,7 +4,7 @@
# Created by: The Resource Compiler for Qt version 6.4.0
# WARNING! All changes made in this file will be lost!
from PySide6 import QtCore
from qtpy import QtCore
qt_resource_data = b"\
\x00\x00\x08\x1c\

View File

@ -0,0 +1,85 @@
import sys
from pathlib import Path
import click
from ..commons import config_options, verbosity_option
from ..config import Config
from ..defaults import CONFIG_PATH
from ..logger import logger
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def wizard(config_path: Path, force: bool, merge: bool) -> None:
"""Launch configuration wizard."""
return _init(config_path, force, merge, skip_interactive=False)
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def init(
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""Initialize a new default configuration file."""
return _init(config_path, force, merge, skip_interactive=True)
def _init(
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""
Actual initialization code for configuration file, with optional interactive
mode.
"""
if config_path.exists():
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
if not force and not merge:
choice = click.prompt(
"Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?",
type=click.Choice(["o", "m", "q"], case_sensitive=False),
)
force = choice == "o"
merge = choice == "m"
if not force and not merge:
logger.debug("Exiting without doing anything")
sys.exit(0)
config = Config()
if force:
logger.debug(f"Overwriting `{config_path}` if exists")
elif merge:
logger.debug("Merging new config into `{config_path}`")
if not skip_interactive:
if config_path.exists():
config = Config.from_file(config_path)
from ..qt_utils import qapp
from .wizard import Wizard
app = qapp()
app.setApplicationName("Manim Slides Wizard")
window = Wizard(config)
window.show()
app.exec()
if window.closed_without_saving:
sys.exit(0)
config = window.config
if merge:
config = Config.from_file(config_path).merge_with(config)
config.to_file(config_path)
click.secho(f"Configuration file successfully saved to `{config_path}`")

View File

@ -1,12 +1,9 @@
import sys
from functools import partial
from pathlib import Path
from typing import Any
import click
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QKeyEvent
from PySide6.QtWidgets import (
from qtpy.QtCore import Qt
from qtpy.QtGui import QIcon, QKeyEvent
from qtpy.QtWidgets import (
QDialog,
QDialogButtonBox,
QGridLayout,
@ -17,12 +14,9 @@ from PySide6.QtWidgets import (
QWidget,
)
from .commons import config_options, verbosity_option
from .config import Config, Key
from .defaults import CONFIG_PATH
from .logger import logger
from .qt_utils import qapp
from .resources import * # noqa: F403
from ..config import Config, Key
from ..logger import logger
from ..resources import * # noqa: F403
WINDOW_NAME: str = "Configuration Wizard"
@ -125,76 +119,3 @@ class Wizard(QWidget): # type: ignore
key_name = keymap[dialog.key]
key.set_ids(dialog.key)
button.setText(key_name)
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def wizard(config_path: Path, force: bool, merge: bool) -> None:
"""Launch configuration wizard."""
return _init(config_path, force, merge, skip_interactive=False)
@click.command()
@config_options
@click.help_option("-h", "--help")
@verbosity_option
def init(
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""Initialize a new default configuration file."""
return _init(config_path, force, merge, skip_interactive=True)
def _init(
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
) -> None:
"""
Actual initialization code for configuration file, with optional interactive
mode.
"""
if config_path.exists():
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
if not force and not merge:
choice = click.prompt(
"Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?",
type=click.Choice(["o", "m", "q"], case_sensitive=False),
)
force = choice == "o"
merge = choice == "m"
if not force and not merge:
logger.debug("Exiting without doing anything")
sys.exit(0)
config = Config()
if force:
logger.debug(f"Overwriting `{config_path}` if exists")
elif merge:
logger.debug("Merging new config into `{config_path}`")
if not skip_interactive:
if config_path.exists():
config = Config.from_file(config_path)
app = qapp()
app.setApplicationName("Manim Slides Wizard")
window = Wizard(config)
window.show()
app.exec()
if window.closed_without_saving:
sys.exit(0)
config = window.config
if merge:
config = Config.from_file(config_path).merge_with(config)
config.to_file(config_path)
click.secho(f"Configuration file successfully saved to `{config_path}`")

1289
pdm.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,10 @@ requires = ["pdm-backend", "setuptools"]
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Multimedia :: Video",
@ -27,8 +27,8 @@ dependencies = [
"pillow>=9.5.0",
"pydantic>=2.0.1",
"pydantic-extra-types>=2.0.0",
"pyside6==6.5.2",
"python-pptx>=0.6.21",
"qtpy>=2.4.1",
"requests>=2.28.1",
"rich>=13.3.2",
"rtoml>=0.9.0",
@ -40,15 +40,19 @@ keywords = ["manim", "slides", "plugin", "manimgl"]
license = {text = "MIT"}
name = "manim-slides"
readme = "README.md"
requires-python = ">=3.8,<3.12"
requires-python = ">=3.9,<3.13"
[project.optional-dependencies]
all = [
full = [
"manim-slides[magic,manim,manimgl,sphinx-directive]",
]
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
manim = ["manim>=0.17.3"]
manimgl = ["manimgl>=1.6.1"]
pyqt6 = ["pyqt6>=6.6.1"]
pyqt6-full = ["manim-slides[full,pyqt6]"]
pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'"]
pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
[project.scripts]
@ -63,10 +67,10 @@ Repository = "https://github.com/jeertmans/manim-slides"
[tool.coverage.report]
exclude_lines = [
'pragma: no cover',
'raise NotImplementedError',
'if TYPE_CHECKING:',
'if typing.TYPE_CHECKING:',
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
]
precision = 2
@ -95,7 +99,7 @@ docs = [
]
github-action = ["setuptools"]
test = [
"manim-slides[manim,manimgl]",
"manim-slides[manim,manimgl,pyqt6]",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
@ -103,6 +107,10 @@ test = [
"pytest-xdist>=3.3.1",
]
[tool.pdm.resolution.overrides]
manimpango = "<1.0.0,>=0.5.0" # This conflicts with ManimGL, hopefully not an issue
skia-pathops = "0.8.0.post1" # From manim 0.18.0 (Python 3.12 support)
[tool.pdm.version]
path = "manim_slides/__version__.py"
source = "file"
@ -134,6 +142,6 @@ extend-ignore = [
]
extend-include = ["*.ipynb"]
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
isort = {known-first-party = ['manim_slides', 'tests']}
isort = {known-first-party = ["manim_slides", "tests"]}
line-length = 88
target-version = "py38"

View File

@ -3,7 +3,7 @@ from typing import Iterator, Tuple
import pytest
from click.testing import CliRunner
from PySide6.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication
from manim_slides.present import present
@ -11,12 +11,12 @@ from manim_slides.present import present
@pytest.fixture(autouse=True)
def auto_shutdown_qapp() -> Iterator[None]:
if app := QApplication.instance():
app.shutdown()
app.quit()
yield
if app := QApplication.instance():
app.shutdown()
app.quit()
@pytest.fixture(scope="session")

View File

@ -1,4 +1,4 @@
from PySide6.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication
from manim_slides.qt_utils import qapp

View File

@ -2,6 +2,7 @@ import random
import shutil
from pathlib import Path
import numpy as np
import pytest
from click.testing import CliRunner
from manim import (
@ -17,6 +18,7 @@ from manim import (
GrowFromCenter,
Text,
)
from packaging import version
from pydantic import ValidationError
from manim_slides.config import PresentationConfig
@ -29,7 +31,13 @@ from manim_slides.slide.manim import Slide
"renderer",
[
"--CE",
"--GL",
pytest.param(
"--GL",
marks=pytest.mark.skipif(
version.parse(np.__version__) >= version.parse("1.25"),
reason="ManimGL requires numpy<1.25, which is outdate",
),
),
],
)
def test_render_basic_slide(

View File

@ -1,17 +1,18 @@
from pathlib import Path
from click.testing import CliRunner
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
from pytest import MonkeyPatch
from pytestqt.qtbot import QtBot
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
QApplication,
QMessageBox,
)
from pytest import MonkeyPatch
from pytestqt.qtbot import QtBot
from manim_slides.config import Config, Key
from manim_slides.defaults import CONFIG_PATH
from manim_slides.wizard import KeyInput, Wizard, init, wizard
from manim_slides.wizard import init, wizard
from manim_slides.wizard.wizard import KeyInput, Wizard
class TestKeyInput: