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>
This commit is contained in:
Jérome Eertmans
2024-07-01 18:19:24 +02:00
committed by GitHub
parent e80d1d08eb
commit e2d8c5667f
11 changed files with 603 additions and 284 deletions

View File

@ -7,7 +7,52 @@ on:
name: Tests name: Tests
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: 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: pytest:
strategy: strategy:
fail-fast: false fail-fast: false
@ -74,15 +119,9 @@ jobs:
- name: Run pytest - name: Run pytest
shell: bash shell: bash
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'
run: rye run pytest 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/
- name: Upload to codecov.io - name: Upload to codecov.io
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
env: env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

1
.gitignore vendored
View File

@ -45,6 +45,7 @@ paper/paper.pdf
paper/media/ paper/media/
# Others # Others
.coverage
coverage.xml coverage.xml
rendering_times.csv rendering_times.csv

View File

@ -15,4 +15,7 @@ sphinx:
fail_on_warning: true fail_on_warning: true
python: python:
install: install:
- requirements: requirements-dev.lock - method: pip
path: .
extra_requirements:
- docs

View File

@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74) [samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74)
is solved. is solved.
[#432](https://github.com/jeertmans/manim-slides/pull/432) [#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)
(unreleased-fixed)= (unreleased-fixed)=
### Fixed ### Fixed
@ -28,12 +34,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed whitespace issue in default RevealJS template. - Fixed whitespace issue in default RevealJS template.
[#442](https://github.com/jeertmans/manim-slides/pull/442) [#442](https://github.com/jeertmans/manim-slides/pull/442)
(unreleased-chore)= (unreleased-removed)=
### Chore ### Removed
- Removed an old validation check that prevented setting `loop=True` with - Removed `full-gl` extra, because it does not make sense to ship both
`auto_next=True` on `next_slide()` `manimgl` and `manim` together.
[#445](https://github.com/jeertmans/manim-slides/pull/445) [#447](https://github.com/jeertmans/manim-slides/pull/447)
(v5.1.7)= (v5.1.7)=
## [v5.1.7](https://github.com/jeertmans/manim-slides/compare/v5.1.6...v5.1.7) ## [v5.1.7](https://github.com/jeertmans/manim-slides/compare/v5.1.6...v5.1.7)

View File

@ -23,10 +23,10 @@ using Manim Slides presentations.
Daniel publishes his presentations on *Cosmology, String Theory and related* Daniel publishes his presentations on *Cosmology, String Theory and related*
topics on his 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).
For example, below are the slides of a seminar he gave titled 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%;"> <div style="position:relative;padding-bottom:56.25%;">
<iframe <iframe

View File

@ -108,8 +108,6 @@ using optional dependencies:
- `full`, to include `magic`, `manim`, and - `full`, to include `magic`, `manim`, and
`sphinx-directive` extras (see below); `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 - `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;

View File

@ -2,6 +2,8 @@ from pathlib import Path
from typing import Any, Optional from typing import Any, Optional
from manim import Scene, ThreeDScene, config 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 ..config import BaseSlideConfig
from .base import BaseSlide from .base import BaseSlide
@ -13,25 +15,40 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
for slides rendering. for slides rendering.
""" """
@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 @property
def _frame_height(self) -> float: def _frame_height(self) -> float:
return config["frame_height"] # type: ignore return self._frame_shape[0]
@property @property
def _frame_width(self) -> float: def _frame_width(self) -> float:
return config["frame_width"] # type: ignore return self._frame_shape[1]
@property @property
def _background_color(self) -> str: def _background_color(self) -> str:
color = self.camera.background_color if isinstance(self.renderer, OpenGLRenderer):
if hex_color := getattr(color, "hex", None): return rgba_to_color(self.renderer.background_color).to_hex() # type: ignore
return hex_color # type: ignore else:
else: # manim>=0.18, see https://github.com/ManimCommunity/manim/pull/3020 return self.renderer.camera.background_color.to_hex() # type: ignore
return color.to_hex() # type: ignore
@property @property
def _resolution(self) -> tuple[int, int]: 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 @property
def _partial_movie_files(self) -> list[Path]: def _partial_movie_files(self) -> list[Path]:

View File

@ -42,20 +42,35 @@ name = "manim-slides"
requires-python = ">=3.9" requires-python = ">=3.9"
[project.optional-dependencies] [project.optional-dependencies]
docs = [
"manim-slides[magic,manim,pyqt6,sphinx-directive]",
"furo>=2023.5.20",
"ipykernel>=6.25.1",
"myst-parser>=2.0.0",
"nbsphinx>=0.9.2",
"pandoc>=2.3",
"sphinx>=7.0.1",
"sphinx-click>=4.4.0",
"sphinx-copybutton>=0.5.1",
"sphinxext-opengraph>=0.7.5",
]
full = [ full = [
"manim-slides[magic,manim,sphinx-directive]", "manim-slides[magic,manim,sphinx-directive]",
] ]
full-gl = [
"manim-slides[magic,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.18.0"]
manimgl = ["manimgl>=1.6.1;python_version<'3.12'"] manimgl = ["manimgl>=1.6.1;python_version<'3.12'"]
pyqt6 = ["pyqt6>=6.6.1"] pyqt6 = ["pyqt6>=6.6.1"]
pyqt6-full = ["manim-slides[full,pyqt6]"] pyqt6-full = ["manim-slides[full,pyqt6]"]
pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'"] pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'", "pyside6>=6.6.1;python_version>='3.12'"]
pyside6-full = ["manim-slides[full,pyside6]"] pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"] sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
tests = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-qt>=4.2.0",
]
[project.scripts] [project.scripts]
manim-slides = "manim_slides.__main__:cli" manim-slides = "manim_slides.__main__:cli"
@ -161,12 +176,13 @@ python_version = "3.9"
strict = true strict = true
[tool.pytest.ini_options] [tool.pytest.ini_options]
env = [ addopts = [
"QT_QPA_PLATFORM=offscreen", "--cov-report=xml",
"--cov=manim_slides",
] ]
filterwarnings = [ env = [
"error", "QT_API=pyside6",
"ignore::DeprecationWarning", "QT_QPA_PLATFORM=offscreen",
] ]
[tool.ruff] [tool.ruff]
@ -193,27 +209,20 @@ extend-ignore = [
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"]}
[tool.ruff.lint.per-file-ignores]
"tests/test_slide.py" = ["N801"]
[tool.rye] [tool.rye]
dev-dependencies = [ dev-dependencies = [
"manim-slides[magic,manim,manimgl,pyqt6,sphinx-directive]",
# dev
"bump-my-version>=0.20.3", "bump-my-version>=0.20.3",
"pre-commit>=3.5.0", "pre-commit>=3.5.0",
# docs
"furo>=2023.5.20",
"ipykernel>=6.25.1",
"myst-parser>=2.0.0",
"nbsphinx>=0.9.2",
"pandoc>=2.3",
"sphinx>=7.0.1",
"sphinx-click>=4.4.0",
"sphinx-copybutton>=0.5.1",
"sphinxext-opengraph>=0.7.5",
# tests
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-qt>=4.2.0",
"pytest-xdist>=3.3.1",
] ]
managed = true managed = true
[tool.uv]
override-dependencies = [
# Bypass constraints from ManimGL
"manimpango>=0.5.0,<1.0.0",
"numpy<=1.24;python_version < '3.12'",
"numpy>=1.26;python_version >= '3.12'",
]

View File

@ -6,6 +6,7 @@
# features: [] # features: []
# all-features: true # all-features: true
# with-sources: false # with-sources: false
# generate-hashes: false
-e file:. -e file:.
alabaster==0.7.16 alabaster==0.7.16
@ -44,12 +45,10 @@ click==8.1.7
# via rich-click # via rich-click
# via sphinx-click # via sphinx-click
click-default-group==1.2.4 click-default-group==1.2.4
# via manim
# via manim-slides # via manim-slides
cloup==0.13.1 cloup==3.0.5
# via manim # via manim
colour==0.1.5 colour==0.1.5
# via manim
# via manimgl # via manimgl
comm==0.2.2 comm==0.2.2
# via ipykernel # via ipykernel
@ -74,17 +73,16 @@ docutils==0.20.1
# via nbsphinx # via nbsphinx
# via sphinx # via sphinx
# via sphinx-click # via sphinx-click
execnet==2.1.1
# via pytest-xdist
executing==2.0.1 executing==2.0.1
# via stack-data # via stack-data
fastjsonschema==2.19.1 fastjsonschema==2.19.1
# via nbformat # via nbformat
filelock==3.13.4 filelock==3.13.4
# via virtualenv # via virtualenv
fonttools==4.51.0 fonttools==4.53.0
# via matplotlib # via matplotlib
furo==2024.1.29 furo==2024.1.29
# via manim-slides
glcontext==2.5.0 glcontext==2.5.0
# via moderngl # via moderngl
identify==2.5.35 identify==2.5.35
@ -96,6 +94,7 @@ imagesize==1.4.1
iniconfig==2.0.0 iniconfig==2.0.0
# via pytest # via pytest
ipykernel==6.29.4 ipykernel==6.29.4
# via manim-slides
ipython==8.18.1 ipython==8.18.1
# via ipykernel # via ipykernel
# via manim-slides # via manim-slides
@ -131,11 +130,12 @@ kiwisolver==1.4.5
lxml==5.2.1 lxml==5.2.1
# via manim-slides # via manim-slides
# via python-pptx # via python-pptx
manim==0.17.3 manim==0.18.1
# via manim-slides # via manim-slides
manimgl==1.6.1 manimgl==1.6.1
# via manim-slides # via manim-slides
manimpango==0.4.4 manimpango==0.5.0
# via --override (workspace)
# via manim # via manim
# via manimgl # via manimgl
mapbox-earcut==1.0.1 mapbox-earcut==1.0.1
@ -148,7 +148,7 @@ markdown-it-py==3.0.0
markupsafe==2.1.5 markupsafe==2.1.5
# via jinja2 # via jinja2
# via nbconvert # via nbconvert
matplotlib==3.8.4 matplotlib==3.9.0
# via manimgl # via manimgl
matplotlib-inline==0.1.7 matplotlib-inline==0.1.7
# via ipykernel # via ipykernel
@ -163,7 +163,7 @@ moderngl==5.10.0
# via manim # via manim
# via manimgl # via manimgl
# via moderngl-window # via moderngl-window
moderngl-window==2.4.4 moderngl-window==2.4.6
# via manim # via manim
# via manimgl # via manimgl
mpmath==1.3.0 mpmath==1.3.0
@ -171,6 +171,7 @@ mpmath==1.3.0
multipledispatch==1.0.0 multipledispatch==1.0.0
# via pyrr # via pyrr
myst-parser==2.0.0 myst-parser==2.0.0
# via manim-slides
nbclient==0.10.0 nbclient==0.10.0
# via nbconvert # via nbconvert
nbconvert==7.16.3 nbconvert==7.16.3
@ -180,14 +181,17 @@ nbformat==5.10.4
# via nbconvert # via nbconvert
# via nbsphinx # via nbsphinx
nbsphinx==0.9.3 nbsphinx==0.9.3
# via manim-slides
nest-asyncio==1.6.0 nest-asyncio==1.6.0
# via ipykernel # via ipykernel
networkx==2.8.8 networkx==2.8.8
# via manim # via manim
nodeenv==1.8.0 nodeenv==1.8.0
# via pre-commit # via pre-commit
numpy==1.26.4 numpy==1.24.0
# via --override (workspace)
# via contourpy # via contourpy
# via ipython
# via isosurfaces # via isosurfaces
# via manim # via manim
# via manim-slides # via manim-slides
@ -195,6 +199,7 @@ numpy==1.26.4
# via mapbox-earcut # via mapbox-earcut
# via matplotlib # via matplotlib
# via moderngl-window # via moderngl-window
# via networkx
# via pyrr # via pyrr
# via scipy # via scipy
packaging==24.0 packaging==24.0
@ -205,13 +210,14 @@ packaging==24.0
# via qtpy # via qtpy
# via sphinx # via sphinx
pandoc==2.3 pandoc==2.3
# via manim-slides
pandocfilters==1.5.1 pandocfilters==1.5.1
# via nbconvert # via nbconvert
parso==0.8.4 parso==0.8.4
# via jedi # via jedi
pexpect==4.9.0 pexpect==4.9.0
# via ipython # via ipython
pillow==9.5.0 pillow==10.3.0
# via manim # via manim
# via manim-slides # via manim-slides
# via manimgl # via manimgl
@ -284,14 +290,16 @@ pyside6-essentials==6.5.2
# via pyside6 # via pyside6
# via pyside6-addons # via pyside6-addons
pytest==8.1.1 pytest==8.1.1
# via manim-slides
# via pytest-cov # via pytest-cov
# via pytest-env # via pytest-env
# via pytest-qt # via pytest-qt
# via pytest-xdist
pytest-cov==5.0.0 pytest-cov==5.0.0
# via manim-slides
pytest-env==1.1.3 pytest-env==1.1.3
# via manim-slides
pytest-qt==4.4.0 pytest-qt==4.4.0
pytest-xdist==3.5.0 # via manim-slides
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
# via jupyter-client # via jupyter-client
# via matplotlib # via matplotlib
@ -314,7 +322,6 @@ referencing==0.34.0
# via jsonschema # via jsonschema
# via jsonschema-specifications # via jsonschema-specifications
requests==2.31.0 requests==2.31.0
# via manim
# via manim-slides # via manim-slides
# via sphinx # via sphinx
rich==13.7.1 rich==13.7.1
@ -346,7 +353,7 @@ six==1.16.0
# via asttokens # via asttokens
# via bleach # via bleach
# via python-dateutil # via python-dateutil
skia-pathops==0.7.4 skia-pathops==0.8.0.post1
# via manim # via manim
# via manimgl # via manimgl
snowballstemmer==2.2.0 snowballstemmer==2.2.0
@ -355,6 +362,7 @@ soupsieve==2.5
# via beautifulsoup4 # via beautifulsoup4
sphinx==7.3.6 sphinx==7.3.6
# via furo # via furo
# via manim-slides
# via myst-parser # via myst-parser
# via nbsphinx # via nbsphinx
# via sphinx-basic-ng # via sphinx-basic-ng
@ -364,7 +372,9 @@ sphinx==7.3.6
sphinx-basic-ng==1.0.0b2 sphinx-basic-ng==1.0.0b2
# via furo # via furo
sphinx-click==5.1.0 sphinx-click==5.1.0
# via manim-slides
sphinx-copybutton==0.5.2 sphinx-copybutton==0.5.2
# via manim-slides
sphinxcontrib-applehelp==1.0.8 sphinxcontrib-applehelp==1.0.8
# via sphinx # via sphinx
sphinxcontrib-devhelp==1.0.6 sphinxcontrib-devhelp==1.0.6
@ -378,6 +388,7 @@ sphinxcontrib-qthelp==1.0.7
sphinxcontrib-serializinghtml==1.1.10 sphinxcontrib-serializinghtml==1.1.10
# via sphinx # via sphinx
sphinxext-opengraph==0.9.1 sphinxext-opengraph==0.9.1
# via manim-slides
srt==3.5.3 srt==3.5.3
# via manim # via manim
stack-data==0.6.3 stack-data==0.6.3
@ -385,7 +396,7 @@ stack-data==0.6.3
svgelements==1.9.6 svgelements==1.9.6
# via manim # via manim
# via manimgl # via manimgl
sympy==1.12 sympy==1.12.1
# via manimgl # via manimgl
tinycss2==1.2.1 tinycss2==1.2.1
# via nbconvert # via nbconvert
@ -410,12 +421,13 @@ traitlets==5.14.2
# via nbformat # via nbformat
# via nbsphinx # via nbsphinx
typing-extensions==4.11.0 typing-extensions==4.11.0
# via manim
# via pydantic # via pydantic
# via pydantic-core # via pydantic-core
# via rich-click # via rich-click
urllib3==2.2.1 urllib3==2.2.1
# via requests # via requests
validators==0.28.0 validators==0.28.3
# via manimgl # via manimgl
virtualenv==20.25.3 virtualenv==20.25.3
# via pre-commit # via pre-commit

View File

@ -6,14 +6,27 @@
# features: [] # features: []
# all-features: true # all-features: true
# with-sources: false # with-sources: false
# generate-hashes: false
-e file:. -e file:.
alabaster==0.7.16
# via sphinx
annotated-types==0.6.0 annotated-types==0.6.0
# via pydantic # via pydantic
asttokens==2.4.1 asttokens==2.4.1
# via stack-data # via stack-data
attrs==23.2.0
# via jsonschema
# via referencing
av==12.0.0 av==12.0.0
# via manim-slides # via manim-slides
babel==2.15.0
# via sphinx
beautifulsoup4==4.12.3
# via furo
# via nbconvert
bleach==6.1.0
# via nbconvert
certifi==2024.2.2 certifi==2024.2.2
# via requests # via requests
charset-normalizer==3.3.2 charset-normalizer==3.3.2
@ -23,32 +36,54 @@ click==8.1.7
# via cloup # via cloup
# via manim # via manim
# via manim-slides # via manim-slides
# via sphinx-click
click-default-group==1.2.4 click-default-group==1.2.4
# via manim
# via manim-slides # via manim-slides
cloup==0.13.1 cloup==3.0.5
# via manim # via manim
colour==0.1.5 colour==0.1.5
# via manim
# via manimgl # via manimgl
comm==0.2.2
# via ipykernel
contourpy==1.2.1 contourpy==1.2.1
# via matplotlib # via matplotlib
coverage==7.5.4
# via pytest-cov
cycler==0.12.1 cycler==0.12.1
# via matplotlib # via matplotlib
debugpy==1.8.2
# via ipykernel
decorator==5.1.1 decorator==5.1.1
# via ipython # via ipython
# via manim # via manim
defusedxml==0.7.1
# via nbconvert
docutils==0.20.1 docutils==0.20.1
# via manim-slides # via manim-slides
# via myst-parser
# via nbsphinx
# via sphinx
# via sphinx-click
executing==2.0.1 executing==2.0.1
# via stack-data # via stack-data
fonttools==4.51.0 fastjsonschema==2.20.0
# via nbformat
fonttools==4.53.0
# via matplotlib # via matplotlib
furo==2024.5.6
# via manim-slides
glcontext==2.5.0 glcontext==2.5.0
# via moderngl # via moderngl
idna==3.7 idna==3.7
# via requests # 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 ipython==8.18.1
# via ipykernel
# via manim-slides # via manim-slides
# via manimgl # via manimgl
isosurfaces==0.1.0 isosurfaces==0.1.0
@ -58,46 +93,90 @@ jedi==0.19.1
# via ipython # via ipython
jinja2==3.1.3 jinja2==3.1.3
# via manim-slides # via manim-slides
# via myst-parser
# via nbconvert
# via nbsphinx
# via sphinx
jsonschema==4.22.0
# via nbformat
jsonschema-specifications==2023.12.1
# via jsonschema
jupyter-client==8.6.2
# 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 kiwisolver==1.4.5
# via matplotlib # via matplotlib
lxml==5.2.1 lxml==5.2.1
# via manim-slides # via manim-slides
# via python-pptx # via python-pptx
manim==0.17.3 manim==0.18.1
# via manim-slides # via manim-slides
manimgl==1.6.1 manimgl==1.6.1
# via manim-slides # via manim-slides
manimpango==0.4.4 manimpango==0.5.0
# via --override (workspace)
# via manim # via manim
# via manimgl # via manimgl
mapbox-earcut==1.0.1 mapbox-earcut==1.0.1
# via manim # via manim
# via manimgl # via manimgl
markdown-it-py==3.0.0 markdown-it-py==3.0.0
# via mdit-py-plugins
# via myst-parser
# via rich # via rich
markupsafe==2.1.5 markupsafe==2.1.5
# via jinja2 # via jinja2
matplotlib==3.8.4 # via nbconvert
matplotlib==3.9.0
# via manimgl # via manimgl
matplotlib-inline==0.1.7 matplotlib-inline==0.1.7
# via ipykernel
# via ipython # via ipython
mdit-py-plugins==0.4.1
# via myst-parser
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
mistune==3.0.2
# via nbconvert
moderngl==5.10.0 moderngl==5.10.0
# via manim # via manim
# via manimgl # via manimgl
# via moderngl-window # via moderngl-window
moderngl-window==2.4.4 moderngl-window==2.4.6
# via manim # via manim
# via manimgl # via manimgl
mpmath==1.3.0 mpmath==1.3.0
# via sympy # via sympy
multipledispatch==1.0.0 multipledispatch==1.0.0
# via pyrr # via pyrr
myst-parser==3.0.1
# via manim-slides
nbclient==0.10.0
# via nbconvert
nbconvert==7.16.4
# via nbsphinx
nbformat==5.10.4
# via nbclient
# via nbconvert
# via nbsphinx
nbsphinx==0.9.4
# via manim-slides
nest-asyncio==1.6.0
# via ipykernel
networkx==2.8.8 networkx==2.8.8
# via manim # via manim
numpy==1.26.4 numpy==1.24.0
# via --override (workspace)
# via contourpy # via contourpy
# via ipython
# via isosurfaces # via isosurfaces
# via manim # via manim
# via manim-slides # via manim-slides
@ -105,24 +184,44 @@ numpy==1.26.4
# via mapbox-earcut # via mapbox-earcut
# via matplotlib # via matplotlib
# via moderngl-window # via moderngl-window
# via networkx
# via pyrr # via pyrr
# via scipy # via scipy
packaging==24.0 packaging==24.0
# via ipykernel
# via matplotlib # via matplotlib
# via nbconvert
# via pytest
# via qtpy # via qtpy
# via sphinx
pandoc==2.3
# via manim-slides
pandocfilters==1.5.1
# via nbconvert
parso==0.8.4 parso==0.8.4
# via jedi # via jedi
pexpect==4.9.0 pexpect==4.9.0
# via ipython # via ipython
pillow==9.5.0 pillow==10.3.0
# via manim # via manim
# via manim-slides # via manim-slides
# via manimgl # via manimgl
# via matplotlib # via matplotlib
# via moderngl-window # via moderngl-window
# via python-pptx # via python-pptx
platformdirs==4.2.2
# via jupyter-core
pluggy==1.5.0
# via pytest
# via pytest-qt
plumbum==1.8.3
# via pandoc
ply==3.11
# via pandoc
prompt-toolkit==3.0.43 prompt-toolkit==3.0.43
# via ipython # via ipython
psutil==6.0.0
# via ipykernel
ptyprocess==0.7.0 ptyprocess==0.7.0
# via pexpect # via pexpect
pure-eval==0.2.2 pure-eval==0.2.2
@ -142,10 +241,13 @@ pydub==0.25.1
pyglet==2.0.15 pyglet==2.0.15
# via moderngl-window # via moderngl-window
pygments==2.17.2 pygments==2.17.2
# via furo
# via ipython # via ipython
# via manim # via manim
# via manimgl # via manimgl
# via nbconvert
# via rich # via rich
# via sphinx
pyopengl==3.1.7 pyopengl==3.1.7
# via manimgl # via manimgl
pyparsing==3.1.2 pyparsing==3.1.2
@ -165,21 +267,43 @@ pyside6-addons==6.5.2
pyside6-essentials==6.5.2 pyside6-essentials==6.5.2
# via pyside6 # via pyside6
# via pyside6-addons # via pyside6-addons
pytest==8.2.2
# via manim-slides
# via pytest-cov
# via pytest-env
# via pytest-qt
pytest-cov==5.0.0
# via manim-slides
pytest-env==1.1.3
# via manim-slides
pytest-qt==4.4.0
# via manim-slides
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
# via jupyter-client
# via matplotlib # via matplotlib
python-pptx==0.6.23 python-pptx==0.6.23
# via manim-slides # via manim-slides
pyyaml==6.0.1 pyyaml==6.0.1
# via manimgl # via manimgl
# via myst-parser
pyzmq==26.0.3
# via ipykernel
# via jupyter-client
qtpy==2.4.1 qtpy==2.4.1
# via manim-slides # via manim-slides
referencing==0.35.1
# via jsonschema
# via jsonschema-specifications
requests==2.31.0 requests==2.31.0
# via manim
# via manim-slides # via manim-slides
# via sphinx
rich==13.7.1 rich==13.7.1
# via manim # via manim
# via manim-slides # via manim-slides
# via manimgl # via manimgl
rpds-py==0.18.1
# via jsonschema
# via referencing
rtoml==0.10.0 rtoml==0.10.0
# via manim-slides # via manim-slides
scipy==1.13.0 scipy==1.13.0
@ -194,10 +318,44 @@ shiboken6==6.5.2
# via pyside6-essentials # via pyside6-essentials
six==1.16.0 six==1.16.0
# via asttokens # via asttokens
# via bleach
# via python-dateutil # via python-dateutil
skia-pathops==0.7.4 skia-pathops==0.8.0.post1
# via manim # via manim
# via manimgl # via manimgl
snowballstemmer==2.2.0
# via sphinx
soupsieve==2.5
# via beautifulsoup4
sphinx==7.3.7
# 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==6.0.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 srt==3.5.3
# via manim # via manim
stack-data==0.6.3 stack-data==0.6.3
@ -205,25 +363,42 @@ stack-data==0.6.3
svgelements==1.9.6 svgelements==1.9.6
# via manim # via manim
# via manimgl # via manimgl
sympy==1.12 sympy==1.12.1
# via manimgl # via manimgl
tinycss2==1.3.0
# via nbconvert
tornado==6.4.1
# via ipykernel
# via jupyter-client
tqdm==4.66.2 tqdm==4.66.2
# via manim # via manim
# via manim-slides # via manim-slides
# via manimgl # via manimgl
traitlets==5.14.2 traitlets==5.14.2
# via comm
# via ipykernel
# via ipython # via ipython
# via jupyter-client
# via jupyter-core
# via matplotlib-inline # via matplotlib-inline
# via nbclient
# via nbconvert
# via nbformat
# via nbsphinx
typing-extensions==4.11.0 typing-extensions==4.11.0
# via manim
# via pydantic # via pydantic
# via pydantic-core # via pydantic-core
urllib3==2.2.1 urllib3==2.2.1
# via requests # via requests
validators==0.28.0 validators==0.28.3
# via manimgl # via manimgl
watchdog==2.3.1 watchdog==2.3.1
# via manim # via manim
wcwidth==0.2.13 wcwidth==0.2.13
# via prompt-toolkit # via prompt-toolkit
webencodings==0.5.1
# via bleach
# via tinycss2
xlsxwriter==3.2.0 xlsxwriter==3.2.0
# via python-pptx # via python-pptx

View File

@ -2,13 +2,11 @@ import random
import shutil import shutil
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any, Union
import manim
import numpy as np
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
from manim import ( from manim import (
BLACK,
BLUE, BLUE,
DOWN, DOWN,
LEFT, LEFT,
@ -21,12 +19,30 @@ from manim import (
GrowFromCenter, GrowFromCenter,
Text, Text,
) )
from packaging import version from manim.renderer.opengl_renderer import OpenGLRenderer
from manim_slides.config import PresentationConfig from manim_slides.config import PresentationConfig
from manim_slides.defaults import FOLDER_PATH from manim_slides.defaults import FOLDER_PATH
from manim_slides.render import render from manim_slides.render import render
from manim_slides.slide.manim import Slide from manim_slides.slide.manim import Slide as CESlide
class CEGLSlide(CESlide):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, renderer=OpenGLRenderer(), **kwargs)
if sys.version_info >= (3, 12):
class _GLSlide:
pass
GLSlide = pytest.param(_GLSlide, marks=pytest.mark.skip())
else:
from manim_slides.slide.manimlib import Slide as GLSlide
SlideType = Union[type[CESlide], type[GLSlide], type[CEGLSlide]]
Slide = Union[CESlide, GLSlide, CEGLSlide]
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -36,8 +52,7 @@ from manim_slides.slide.manim import Slide
pytest.param( pytest.param(
"--GL", "--GL",
marks=pytest.mark.skipif( marks=pytest.mark.skipif(
version.parse(np.__version__) >= version.parse("1.25") sys.version_info >= (3, 12),
or sys.version_info >= (3, 12),
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12", reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
), ),
), ),
@ -82,271 +97,315 @@ def test_render_basic_slide(
assert local_presentation_config.resolution == presentation_config.resolution assert local_presentation_config.resolution == presentation_config.resolution
def assert_constructs(cls: type) -> type: def init_slide(cls: SlideType) -> Slide:
class Wrapper: if issubclass(cls, CESlide):
@classmethod return cls()
def test_construct(_) -> None: # noqa: N804 elif issubclass(cls, GLSlide):
cls().construct() from manimlib.config import get_configuration, parse_cli
from manimlib.extract_scene import get_scene_config
return Wrapper args = parse_cli()
config = get_configuration(args)
scene_config = get_scene_config(config)
return cls(**scene_config)
raise ValueError(f"Unsupported class {cls}")
def assert_renders(cls: type) -> type: parametrize_base_cls = pytest.mark.parametrize(
class Wrapper: "base_cls", (CESlide, GLSlide, CEGLSlide), ids=("CE", "GL", "CE(GL)")
@classmethod )
def test_render(_) -> None: # noqa: N804
cls().render()
return Wrapper
def assert_constructs(cls: SlideType) -> None:
init_slide(cls).construct()
def assert_renders(cls: SlideType) -> None:
init_slide(cls).render()
class TestSlide: class TestSlide:
@assert_constructs def test_default_properties(self) -> None:
class TestDefaultProperties(Slide): @assert_constructs
def construct(self) -> None: class _(CESlide):
assert self._output_folder == FOLDER_PATH def construct(self) -> None:
assert len(self._slides) == 0 assert self._output_folder == FOLDER_PATH
assert self._current_slide == 1 assert len(self._slides) == 0
assert self._start_animation == 0 assert self._current_slide == 1
assert len(self._canvas) == 0 assert self._start_animation == 0
assert self._wait_time_between_slides == 0.0 assert len(self._canvas) == 0
assert self._wait_time_between_slides == 0.0
@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)
@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)
@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
@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
def test_multiple_animations_in_last_slide(self) -> None:
@assert_renders
class _(CESlide):
"""Check against solution for issue #161."""
def construct(self) -> None:
circle = Circle(color=BLUE)
dot = Dot()
@pytest.mark.skipif( self.play(GrowFromCenter(circle))
version.parse(manim.__version__) < version.parse("0.18"), self.play(FadeIn(dot))
reason="Manim change how color are represented in 0.18", self.next_slide()
)
@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()
@assert_renders self.play(dot.animate.move_to(RIGHT))
class TestMultipleAnimationsInLastSlide(Slide): self.play(dot.animate.move_to(UP))
"""Check against solution for issue #161.""" self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN))
def construct(self) -> None: def test_file_too_long(self) -> None:
circle = Circle(color=BLUE) @assert_renders
dot = Dot() class _(CESlide):
"""Check against solution for issue #123."""
self.play(GrowFromCenter(circle)) def construct(self) -> None:
self.play(FadeIn(dot)) circle = Circle(radius=3, color=BLUE)
self.next_slide() dot = Dot()
self.play(GrowFromCenter(circle), run_time=0.1)
self.play(dot.animate.move_to(RIGHT)) for _ in range(30):
self.play(dot.animate.move_to(UP)) direction = (random.random() - 0.5) * LEFT + (
self.play(dot.animate.move_to(LEFT)) random.random() - 0.5
self.play(dot.animate.move_to(DOWN)) ) * UP
self.play(dot.animate.move_to(direction), run_time=0.1)
self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
@assert_renders def test_loop(self) -> None:
class TestFileTooLong(Slide): @assert_constructs
"""Check against solution for issue #123.""" class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
def construct(self) -> None: self.add(text)
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle), run_time=0.1)
for _ in range(30): assert not self._base_slide_config.loop
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 self.next_slide(loop=True)
class TestLoop(Slide): self.play(text.animate.scale(2))
def construct(self) -> None:
text = Text("Some text")
self.add(text) assert self._base_slide_config.loop
assert not self._base_slide_config.loop self.next_slide(loop=False)
self.next_slide(loop=True) assert not self._base_slide_config.loop
self.play(text.animate.scale(2))
assert self._base_slide_config.loop def test_auto_next(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
self.next_slide(loop=False) self.add(text)
assert not self._base_slide_config.loop assert not self._base_slide_config.auto_next
@assert_constructs self.next_slide(auto_next=True)
class TestAutoNext(Slide): self.play(text.animate.scale(2))
def construct(self) -> None:
text = Text("Some text")
self.add(text) assert self._base_slide_config.auto_next
assert not self._base_slide_config.auto_next self.next_slide(auto_next=False)
self.next_slide(auto_next=True) assert not self._base_slide_config.auto_next
self.play(text.animate.scale(2))
assert self._base_slide_config.auto_next def test_loop_and_auto_next_succeeds(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
self.next_slide(auto_next=False) self.add(text)
assert not self._base_slide_config.auto_next self.next_slide(loop=True, auto_next=True)
self.play(text.animate.scale(2))
@assert_constructs self.next_slide()
class TestLoopAndAutoNextSucceeds(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text) def test_playback_rate(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
self.next_slide(loop=True, auto_next=True) self.add(text)
self.play(text.animate.scale(2))
self.next_slide() assert self._base_slide_config.playback_rate == 1.0
@assert_constructs self.next_slide(playback_rate=2.0)
class TestPlaybackRate(Slide): self.play(text.animate.scale(2))
def construct(self) -> None:
text = Text("Some text")
self.add(text) assert self._base_slide_config.playback_rate == 2.0
assert self._base_slide_config.playback_rate == 1.0 def test_reversed_playback_rate(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
self.next_slide(playback_rate=2.0) self.add(text)
self.play(text.animate.scale(2))
assert self._base_slide_config.playback_rate == 2.0 assert self._base_slide_config.reversed_playback_rate == 1.0
@assert_constructs self.next_slide(reversed_playback_rate=2.0)
class TestReversedPlaybackRate(Slide): self.play(text.animate.scale(2))
def construct(self) -> None:
text = Text("Some text")
self.add(text) assert self._base_slide_config.reversed_playback_rate == 2.0
assert self._base_slide_config.reversed_playback_rate == 1.0 def test_notes(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
self.next_slide(reversed_playback_rate=2.0) self.add(text)
self.play(text.animate.scale(2))
assert self._base_slide_config.reversed_playback_rate == 2.0 assert self._base_slide_config.notes == ""
@assert_constructs self.next_slide(notes="test")
class TestNotes(Slide): self.play(text.animate.scale(2))
def construct(self) -> None:
text = Text("Some text")
self.add(text) assert self._base_slide_config.notes == "test"
assert self._base_slide_config.notes == "" def test_wipe(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.next_slide(notes="test") self.add(text)
self.play(text.animate.scale(2))
assert self._base_slide_config.notes == "test" assert text in self.mobjects
assert bye not in self.mobjects
@assert_constructs self.wipe([text], [bye])
class TestWipe(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.add(text) assert text not in self.mobjects
assert bye in self.mobjects
assert text in self.mobjects def test_zoom(self) -> None:
assert bye not in self.mobjects @assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.wipe([text], [bye]) self.add(text)
assert text not in self.mobjects assert text in self.mobjects
assert bye in self.mobjects assert bye not in self.mobjects
@assert_constructs self.zoom([text], [bye])
class TestZoom(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.add(text) assert text not in self.mobjects
assert bye in self.mobjects
assert text in self.mobjects def test_animation_count(self) -> None:
assert bye not in self.mobjects @assert_constructs
class _(CESlide):
def construct(self) -> None:
assert self._current_animation == 0
circle = Circle(color=BLUE)
dot = Dot()
self.zoom([text], [bye]) self.play(GrowFromCenter(circle))
assert self._current_animation == 1
self.play(FadeIn(dot))
assert self._current_animation == 2
assert text not in self.mobjects def test_wait_time_between_slides(self) -> None:
assert bye in self.mobjects @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
@assert_constructs def test_next_slide(self) -> None:
class TestPlay(Slide): @assert_constructs
def construct(self) -> None: class _(CESlide):
assert self._current_animation == 0 def construct(self) -> None:
circle = Circle(color=BLUE) assert self._current_slide == 1
dot = Dot() 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.play(GrowFromCenter(circle)) def test_canvas(self) -> None:
assert self._current_animation == 1 @assert_constructs
self.play(FadeIn(dot)) class _(CESlide):
assert self._current_animation == 2 def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
@assert_constructs assert len(self.canvas) == 0
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
@assert_constructs self.add(text)
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
@assert_constructs assert len(self.canvas) == 0
class TestCanvas(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
assert len(self.canvas) == 0 self.add_to_canvas(text=text)
self.add(text) assert len(self.canvas) == 1
assert len(self.canvas) == 0 self.add(bye)
self.add_to_canvas(text=text) assert len(self.canvas) == 1
assert len(self.canvas) == 1 assert text not in self.mobjects_without_canvas
assert bye in self.mobjects_without_canvas
self.add(bye) self.remove(text)
assert len(self.canvas) == 1 assert len(self.canvas) == 1
assert text not in self.mobjects_without_canvas self.add_to_canvas(bye=bye)
assert bye in self.mobjects_without_canvas
self.remove(text) assert len(self.canvas) == 2
assert len(self.canvas) == 1 self.remove_from_canvas("text", "bye")
self.add_to_canvas(bye=bye) assert len(self.canvas) == 0
assert len(self.canvas) == 2 with pytest.raises(KeyError):
self.remove_from_canvas("text")
self.remove_from_canvas("text", "bye")
assert len(self.canvas) == 0
with pytest.raises(KeyError):
self.remove_from_canvas("text")