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 uses: nikeee/setup-pandoc@v1
- name: Install local Python package - name: Install local Python package
run: pdm install -dGdocs -dGgithub-action run: pdm sync -Gdocs -Ggithub-action
- name: Install IPython kernel - name: Install IPython kernel
run: pdm run ipython kernel install --name "manim-slides" --user run: pdm run ipython kernel install --name "manim-slides" --user

View File

@ -13,7 +13,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-latest, ubuntu-latest, windows-latest] 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 }} runs-on: ${{ matrix.os }}
env: env:
QT_QPA_PLATFORM: offscreen QT_QPA_PLATFORM: offscreen
@ -69,7 +69,7 @@ jobs:
- name: Install Manim Slides - name: Install Manim Slides
run: | run: |
pdm install -dGgithub-action -dGtest pdm sync -Ggithub-action -Gtest
- name: Run pytest - name: Run pytest
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11' 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) [#335](https://github.com/jeertmans/manim-slides/pull/335)
- Changed build backend to PDM and reflected on docs. - Changed build backend to PDM and reflected on docs.
[#354](https://github.com/jeertmans/manim-slides/pull/354) [#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. - Documentated how to create and use a custom HTML template.
[#357](https://github.com/jeertmans/manim-slides/pull/357) [#357](https://github.com/jeertmans/manim-slides/pull/357)

View File

@ -88,11 +88,15 @@ manim-slides render example.py BasicExample
# or use ManimGL # or use ManimGL
manim-slides render --GL example.py BasicExample manim-slides render --GL example.py BasicExample
``` ```
<!-- end usage -->
> [!NOTE] > [!NOTE]
> Using `manim-slides render` makes sure the use the `manim` > Using `manim-slides render` makes sure to use the `manim`
> (or `manimlib`) library that was installed in the Python same environment. > (or `manimlib`) library that was installed in the same Python environment.
> Put simply, this is a wrapper of `manim render [ARGS]...` (or `manimgl [ARGS]...`). > 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: To start the presentation using `Scene1`, `Scene2` and so on, run:
@ -106,7 +110,7 @@ In our example:
manim-slides BasicExample manim-slides BasicExample
``` ```
<!-- end usage --> <!-- end more-usage -->
<p align="center"> <p align="center">
<img alt="Example GIF" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/example.gif"> <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: Additionnally, Manim Slides comes with groups of dependencies for development purposes:
```bash ```bash
pdm install -dGdev # For linters and formatters pdm install -Gdev # For linters and formatters
# or # or
pdm install --dGdocs # To build the documentation locally pdm install -Gdocs # To build the documentation locally
# or # or
pdm install --dGtests # To run tests pdm install -Gtest # To run tests
``` ```
:::{note} :::{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` 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! for ManimGL), otherwise it might install an unsupported version of Manim!
See [#314](https://github.com/jeertmans/manim-slides/issues/314). 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 --> <!-- end deps -->
## Pip Install ## 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 ```bash
pipx install -U manim-slides pipx install -U "manim-slides[pyside6-full]"
``` ```
:::{tip} :::{tip}
@ -52,6 +59,28 @@ like to upgrade to the latest version available,
if Manim Slides is already installed. 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]: Optionally, you can also install Manim or ManimGL using extras[^1]:
```bash ```bash
@ -60,11 +89,8 @@ pipx install -U "manim-slides[manim]" # For Manim
pipx install -U "manim-slides[manimgl]" # For ManimGL pipx install -U "manim-slides[manimgl]" # For ManimGL
``` ```
You can check that Manim Slides was correctly installed with: For optional dependencies documentation, see
[next section](#optional-dependencies).
```bash
manim-slides --version
```
:::{warning} :::{warning}
If you are installing with pipx, this is mandatory to at least include 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 [^1]: You still need to have Manim or ManimGL platform-specific dependencies
installed on your computer. installed on your computer.
## Optional Dependencies ## Optional dependencies
Along with the optional dependencies for Manim and ManimGL, Along with the optional dependencies for Manim and ManimGL,
Manim Slides offers additional *extras*, that can be activated Manim Slides offers additional *extras*, that can be activated
using optional dependencies: using optional dependencies:
- `full`, to include `magic`, `manim`, `manimgl`, and
`sphinx-directive` extras (see below);
- `magic`, to include a Jupyter magic to render - `magic`, to include a Jupyter magic to render
animations inside notebooks. This automatically installs `manim`, animations inside notebooks. This automatically installs `manim`,
and does not work with ManimGL; and does not work with ManimGL;
- `manim` and `manimgl`, for installing the corresponding - `manim` and `manimgl`, for installing the corresponding
dependencies; 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 - `sphinx-directive`, to generate presentation inside your Sphinx
documentation. This automatically installs `manim`, 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: 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]" pipx install -U "manim-slides[extra1,extra2]"
``` ```
:::{note} [^2]: Actually, PySide6 can be installed on Python 3.12, but you will then
The quotes `"` are added because not all shell support unquoted observe the same visual bug as with PyQt6.
brackets (e.g., zsh) or commas (e.g., Windows).
:::
## 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, 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) [contributing guide](./contributing/workflow)
to know how to process. to know how to process.

View File

@ -10,6 +10,19 @@ see [installation](./installation).
:end-before: <!-- end usage --> :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: The output slides should look this this:
```{eval-rst} ```{eval-rst}

View File

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

View File

@ -648,14 +648,16 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"config_options", "config_options",
multiple=True, multiple=True,
callback=validate_config_option, 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( @click.option(
"--use-template", "--use-template",
"template", "template",
metavar="FILE", metavar="FILE",
type=click.Path(exists=True, dir_okay=False, path_type=Path), 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_template_option
@show_config_options @show_config_options

View File

@ -6,18 +6,10 @@ from typing import List, Optional, Tuple
import click import click
from click import Context, Parameter from click import Context, Parameter
from pydantic import ValidationError from pydantic import ValidationError
from PySide6.QtCore import Qt
from ..commons import config_path_option, folder_path_option, verbosity_option from ..commons import config_path_option, folder_path_option, verbosity_option
from ..config import Config, PresentationConfig from ..config import Config, PresentationConfig
from ..logger import logger from ..logger import logger
from ..qt_utils import qapp
from .player import Player
ASPECT_RATIO_MODES = {
"keep": Qt.KeepAspectRatio,
"ignore": Qt.IgnoreAspectRatio,
}
@click.command() @click.command()
@ -130,7 +122,8 @@ def start_at_callback(
return tuple(map(str_to_int_or_none, values_tuple)) return tuple(map(str_to_int_or_none, values_tuple))
raise click.BadParameter( 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, ctx=ctx,
param=param, param=param,
) )
@ -283,6 +276,8 @@ def present(
if start_at[1]: if start_at[1]:
start_at_slide_number = start_at[1] start_at_slide_number = start_at[1]
from ..qt_utils import qapp
app = qapp() app = qapp()
app.setApplicationName("Manim Slides") app.setApplicationName("Manim Slides")
@ -298,6 +293,15 @@ def present(
else: else:
screen = None screen = None
from qtpy.QtCore import Qt
aspect_ratio_modes = {
"keep": Qt.KeepAspectRatio,
"ignore": Qt.IgnoreAspectRatio,
}
from .player import Player
player = Player( player = Player(
config, config,
presentation_configs, presentation_configs,
@ -306,7 +310,7 @@ def present(
skip_all=skip_all, skip_all=skip_all,
exit_after_last_slide=exit_after_last_slide, exit_after_last_slide=exit_after_last_slide,
hide_mouse=hide_mouse, 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, presentation_index=start_at_scene_number,
slide_index=start_at_slide_number, slide_index=start_at_slide_number,
screen=screen, screen=screen,

View File

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

View File

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

View File

@ -4,7 +4,7 @@
# Created by: The Resource Compiler for Qt version 6.4.0 # Created by: The Resource Compiler for Qt version 6.4.0
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
from PySide6 import QtCore from qtpy import QtCore
qt_resource_data = b"\ qt_resource_data = b"\
\x00\x00\x08\x1c\ \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 functools import partial
from pathlib import Path
from typing import Any from typing import Any
import click from qtpy.QtCore import Qt
from PySide6.QtCore import Qt from qtpy.QtGui import QIcon, QKeyEvent
from PySide6.QtGui import QIcon, QKeyEvent from qtpy.QtWidgets import (
from PySide6.QtWidgets import (
QDialog, QDialog,
QDialogButtonBox, QDialogButtonBox,
QGridLayout, QGridLayout,
@ -17,12 +14,9 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from .commons import config_options, verbosity_option from ..config import Config, Key
from .config import Config, Key from ..logger import logger
from .defaults import CONFIG_PATH from ..resources import * # noqa: F403
from .logger import logger
from .qt_utils import qapp
from .resources import * # noqa: F403
WINDOW_NAME: str = "Configuration Wizard" WINDOW_NAME: str = "Configuration Wizard"
@ -125,76 +119,3 @@ class Wizard(QWidget): # type: ignore
key_name = keymap[dialog.key] key_name = keymap[dialog.key]
key.set_ids(dialog.key) key.set_ids(dialog.key)
button.setText(key_name) 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"}] authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
classifiers = [ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Topic :: Multimedia :: Video", "Topic :: Multimedia :: Video",
@ -27,8 +27,8 @@ dependencies = [
"pillow>=9.5.0", "pillow>=9.5.0",
"pydantic>=2.0.1", "pydantic>=2.0.1",
"pydantic-extra-types>=2.0.0", "pydantic-extra-types>=2.0.0",
"pyside6==6.5.2",
"python-pptx>=0.6.21", "python-pptx>=0.6.21",
"qtpy>=2.4.1",
"requests>=2.28.1", "requests>=2.28.1",
"rich>=13.3.2", "rich>=13.3.2",
"rtoml>=0.9.0", "rtoml>=0.9.0",
@ -40,15 +40,19 @@ keywords = ["manim", "slides", "plugin", "manimgl"]
license = {text = "MIT"} license = {text = "MIT"}
name = "manim-slides" name = "manim-slides"
readme = "README.md" readme = "README.md"
requires-python = ">=3.8,<3.12" requires-python = ">=3.9,<3.13"
[project.optional-dependencies] [project.optional-dependencies]
all = [ full = [
"manim-slides[magic,manim,manimgl,sphinx-directive]", "manim-slides[magic,manim,manimgl,sphinx-directive]",
] ]
magic = ["manim-slides[manim]", "ipython>=8.12.2"] magic = ["manim-slides[manim]", "ipython>=8.12.2"]
manim = ["manim>=0.17.3"] manim = ["manim>=0.17.3"]
manimgl = ["manimgl>=1.6.1"] 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]"] sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
[project.scripts] [project.scripts]
@ -63,10 +67,10 @@ Repository = "https://github.com/jeertmans/manim-slides"
[tool.coverage.report] [tool.coverage.report]
exclude_lines = [ exclude_lines = [
'pragma: no cover', "pragma: no cover",
'raise NotImplementedError', "raise NotImplementedError",
'if TYPE_CHECKING:', "if TYPE_CHECKING:",
'if typing.TYPE_CHECKING:', "if typing.TYPE_CHECKING:",
] ]
precision = 2 precision = 2
@ -95,7 +99,7 @@ docs = [
] ]
github-action = ["setuptools"] github-action = ["setuptools"]
test = [ test = [
"manim-slides[manim,manimgl]", "manim-slides[manim,manimgl,pyqt6]",
"pytest>=7.4.0", "pytest>=7.4.0",
"pytest-cov>=4.1.0", "pytest-cov>=4.1.0",
"pytest-env>=0.8.2", "pytest-env>=0.8.2",
@ -103,6 +107,10 @@ test = [
"pytest-xdist>=3.3.1", "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] [tool.pdm.version]
path = "manim_slides/__version__.py" path = "manim_slides/__version__.py"
source = "file" source = "file"
@ -134,6 +142,6 @@ extend-ignore = [
] ]
extend-include = ["*.ipynb"] extend-include = ["*.ipynb"]
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"] 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 line-length = 88
target-version = "py38" target-version = "py38"

View File

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

View File

@ -2,6 +2,7 @@ import random
import shutil import shutil
from pathlib import Path from pathlib import Path
import numpy as np
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from manim import ( from manim import (
@ -17,6 +18,7 @@ from manim import (
GrowFromCenter, GrowFromCenter,
Text, Text,
) )
from packaging import version
from pydantic import ValidationError from pydantic import ValidationError
from manim_slides.config import PresentationConfig from manim_slides.config import PresentationConfig
@ -29,7 +31,13 @@ from manim_slides.slide.manim import Slide
"renderer", "renderer",
[ [
"--CE", "--CE",
pytest.param(
"--GL", "--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( def test_render_basic_slide(

View File

@ -1,17 +1,18 @@
from pathlib import Path from pathlib import Path
from click.testing import CliRunner from click.testing import CliRunner
from PySide6.QtCore import Qt from pytest import MonkeyPatch
from PySide6.QtWidgets import ( from pytestqt.qtbot import QtBot
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
QApplication, QApplication,
QMessageBox, QMessageBox,
) )
from pytest import MonkeyPatch
from pytestqt.qtbot import QtBot
from manim_slides.config import Config, Key from manim_slides.config import Config, Key
from manim_slides.defaults import CONFIG_PATH 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: class TestKeyInput: