chore(deps): bumping pydantic to V2 (#207)

* chore(deps): bumping pydantic to V2

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

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

* fix(lib): validators

* fix(ci): add tests and fixes

* fix(lib): add missing mode arg

* fix(lib): change function name

* chore(tests): add more tests and use pytest

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

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

* chore(deps): change test deps

* chore(ci): install manim deps

* fix(ci): move to right place

* fix(lib): add custom schema

* fix(lib): validators

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jérome Eertmans
2023-07-05 14:35:58 +02:00
committed by GitHub
parent e1d5fb732c
commit 9279d2a22a
10 changed files with 578 additions and 327 deletions

View File

@ -1,21 +1,40 @@
on: on:
pull_request: pull_request:
paths:
- pyproject.toml
- poetry.lock
- '**.py'
- .github/workflows/test_examples.yml
workflow_dispatch: workflow_dispatch:
name: Test Examples name: Tests
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: debug
PYTHONFAULTHANDLER: 1
DISPLAY: :99
jobs: jobs:
pytest:
strategy:
fail-fast: false
matrix:
pyversion: ['3.8', '3.9', '3.10', '3.11']
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Poetry
run: pipx install poetry
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.pyversion }}
cache: poetry
- name: Install manim dependencies on Ubuntu
run: |
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Install Manim Slides
run: |
poetry install --with test
- name: Run pytest
run: poetry run pytest
build-examples: build-examples:
strategy: strategy:
fail-fast: false fail-fast: false
@ -45,11 +64,18 @@ jobs:
pyversion: '3.10' pyversion: '3.10'
manim: manim manim: manim
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: debug
PYTHONFAULTHANDLER: 1
DISPLAY: :99
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Poetry - name: Install Poetry
run: pipx install poetry run: pipx install poetry
- name: Install Python - name: Install Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
@ -62,9 +88,11 @@ jobs:
run: | run: |
echo "${HOME}/.local/bin" >> $GITHUB_PATH echo "${HOME}/.local/bin" >> $GITHUB_PATH
echo "/Users/runner/Library/Python/${{ matrix.pyversion }}/bin" >> $GITHUB_PATH echo "/Users/runner/Library/Python/${{ matrix.pyversion }}/bin" >> $GITHUB_PATH
- name: Append to Path on Ubuntu - name: Append to Path on Ubuntu
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: echo "${HOME}/.local/bin" >> $GITHUB_PATH run: echo "${HOME}/.local/bin" >> $GITHUB_PATH
- name: Append to Path on Windows - name: Append to Path on Windows
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
run: echo "${HOME}/.local/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append run: echo "${HOME}/.local/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
@ -73,25 +101,31 @@ jobs:
- name: Install manim dependencies on MacOs - name: Install manim dependencies on MacOs
if: matrix.os == 'macos-latest' && matrix.manim == 'manim' if: matrix.os == 'macos-latest' && matrix.manim == 'manim'
run: brew install ffmpeg py3cairo run: brew install ffmpeg py3cairo
- name: Install manimgl dependencies on MacOS - name: Install manimgl dependencies on MacOS
if: matrix.os == 'macos-latest' && matrix.manim == 'manimgl' if: matrix.os == 'macos-latest' && matrix.manim == 'manimgl'
run: brew install ffmpeg run: brew install ffmpeg
- name: Run apt-get update on Ubuntu - name: Run apt-get update on Ubuntu
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: sudo apt-get update run: sudo apt-get update
- name: Install manim dependencies on Ubuntu - name: Install manim dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manim' if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manim'
run: | run: |
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
- name: Install manimgl dependencies on Ubuntu - name: Install manimgl dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl' if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl'
run: | run: |
sudo apt-get install libpango1.0-dev ffmpeg freeglut3-dev sudo apt-get install libpango1.0-dev ffmpeg freeglut3-dev
- name: Install xvfb on Ubuntu - name: Install xvfb on Ubuntu
if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl' if: matrix.os == 'ubuntu-latest' && matrix.manim == 'manimgl'
run: | run: |
sudo apt-get install xvfb sudo apt-get install xvfb
nohup Xvfb $DISPLAY & nohup Xvfb $DISPLAY &
- name: Install Windows dependencies - name: Install Windows dependencies
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
run: choco install ffmpeg run: choco install ffmpeg
@ -99,12 +133,13 @@ jobs:
# Install Manim Slides # Install Manim Slides
- name: Install Manim Slides - name: Install Manim Slides
run: | run: |
poetry install --with test poetry install --extras ${{ matrix.manim }}
# Render slides # Render slides
- name: Render slides - name: Render slides
if: matrix.manim == 'manim' if: matrix.manim == 'manim'
run: poetry run manim -ql example.py BasicExample ThreeDExample run: poetry run manim -ql example.py BasicExample ThreeDExample
- name: Render slides - name: Render slides
if: matrix.manim == 'manimgl' if: matrix.manim == 'manimgl'
run: poetry run -v manimgl -l example.py BasicExample ThreeDExample run: poetry run -v manimgl -l example.py BasicExample ThreeDExample

1
.gitignore vendored
View File

@ -1,6 +1,5 @@
__pycache__/ __pycache__/
/env /env
/tests
/build /build
/dist /dist
*.egg-info/ *.egg-info/

View File

@ -5,10 +5,10 @@ import subprocess
import tempfile import tempfile
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple, Union from typing import Any, Dict, List, Optional, Set, Tuple, Union
from pydantic import BaseModel, FilePath, PositiveInt, root_validator, validator from pydantic import BaseModel, FilePath, PositiveInt, field_validator, model_validator
from pydantic.color import Color from pydantic_extra_types.color import Color
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from .defaults import FFMPEG_BIN from .defaults import FFMPEG_BIN
@ -38,18 +38,19 @@ def merge_basenames(files: List[FilePath]) -> Path:
class Key(BaseModel): # type: ignore class Key(BaseModel): # type: ignore
"""Represents a list of key codes, with optionally a name.""" """Represents a list of key codes, with optionally a name."""
ids: Set[int] ids: Set[PositiveInt]
name: Optional[str] = None name: Optional[str] = None
@field_validator("ids")
@classmethod
def ids_is_non_empty_set(cls, ids: Set[Any]) -> Set[Any]:
if len(ids) <= 0:
raise ValueError("Key's ids must be a non-empty set")
return ids
def set_ids(self, *ids: int) -> None: def set_ids(self, *ids: int) -> None:
self.ids = set(ids) self.ids = set(ids)
@validator("ids", each_item=True)
def id_is_posint(cls, v: int) -> int:
if v < 0:
raise ValueError("Key ids cannot be negative integers")
return v
def match(self, key_id: int) -> bool: def match(self, key_id: int) -> bool:
m = key_id in self.ids m = key_id in self.ids
@ -70,7 +71,7 @@ class Config(BaseModel): # type: ignore
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE") PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE") HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
@root_validator @model_validator(mode="before")
def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]: def ids_are_unique_across_keys(cls, values: Dict[str, Key]) -> Dict[str, Key]:
ids: Set[int] = set() ids: Set[int] = set()
@ -105,19 +106,21 @@ class SlideConfig(BaseModel): # type: ignore
number: int number: int
terminated: bool = False terminated: bool = False
@validator("start_animation", "end_animation") @field_validator("start_animation", "end_animation")
@classmethod
def index_is_posint(cls, v: int) -> int: def index_is_posint(cls, v: int) -> int:
if v < 0: if v < 0:
raise ValueError("Animation index (start or end) cannot be negative") raise ValueError("Animation index (start or end) cannot be negative")
return v return v
@validator("number") @field_validator("number")
@classmethod
def number_is_strictly_posint(cls, v: int) -> int: def number_is_strictly_posint(cls, v: int) -> int:
if v <= 0: if v <= 0:
raise ValueError("Slide number cannot be negative or zero") raise ValueError("Slide number cannot be negative or zero")
return v return v
@root_validator @model_validator(mode="before")
def start_animation_is_before_end( def start_animation_is_before_end(
cls, values: Dict[str, Union[SlideType, int, bool]] cls, values: Dict[str, Union[SlideType, int, bool]]
) -> Dict[str, Union[SlideType, int, bool]]: ) -> Dict[str, Union[SlideType, int, bool]]:
@ -153,15 +156,12 @@ class PresentationConfig(BaseModel): # type: ignore
resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080) resolution: Tuple[PositiveInt, PositiveInt] = (1920, 1080)
background_color: Color = "black" background_color: Color = "black"
@root_validator @model_validator(mode="after")
def animation_indices_match_files( def animation_indices_match_files(
cls, values: Dict[str, Union[List[SlideConfig], List[FilePath]]] cls, config: "PresentationConfig"
) -> Dict[str, Union[List[SlideConfig], List[FilePath]]]: ) -> "PresentationConfig":
files: List[FilePath] = values.get("files") # type: ignore files = config.files
slides: List[SlideConfig] = values.get("slides") # type: ignore slides = config.slides
if files is None or slides is None:
return values
n_files = len(files) n_files = len(files)
@ -171,7 +171,7 @@ class PresentationConfig(BaseModel): # type: ignore
f"The following slide's contains animations not listed in files {files}: {slide}" f"The following slide's contains animations not listed in files {files}: {slide}"
) )
return values return config
def copy_to(self, dest: Path, use_cached: bool = True) -> "PresentationConfig": def copy_to(self, dest: Path, use_cached: bool = True) -> "PresentationConfig":
""" """

View File

@ -15,7 +15,16 @@ import pptx
from click import Context, Parameter from click import Context, Parameter
from lxml import etree from lxml import etree
from PIL import Image from PIL import Image
from pydantic import BaseModel, FilePath, PositiveFloat, PositiveInt, ValidationError from pydantic import (
BaseModel,
ConfigDict,
FilePath,
GetCoreSchemaHandler,
PositiveFloat,
PositiveInt,
ValidationError,
)
from pydantic_core import CoreSchema, core_schema
from tqdm import tqdm from tqdm import tqdm
from . import data from . import data
@ -87,6 +96,12 @@ class Str(str):
# This fixes pickling issue on Python 3.8 # This fixes pickling issue on Python 3.8
__reduce_ex__ = str.__reduce_ex__ __reduce_ex__ = str.__reduce_ex__
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
return core_schema.str_schema()
def __str__(self) -> str: def __str__(self) -> str:
"""Ensures that the string is correctly quoted.""" """Ensures that the string is correctly quoted."""
if self in ["true", "false", "null"]: if self in ["true", "false", "null"]:
@ -304,10 +319,7 @@ class RevealJS(Converter):
reveal_version: str = "4.4.0" reveal_version: str = "4.4.0"
reveal_theme: RevealTheme = RevealTheme.black reveal_theme: RevealTheme = RevealTheme.black
title: str = "Manim Slides" title: str = "Manim Slides"
model_config = ConfigDict(use_enum_values=True, extra="forbid")
class Config:
use_enum_values = True
extra = "forbid"
def get_sections_iter(self, assets_dir: Path) -> Generator[str, None, None]: def get_sections_iter(self, assets_dir: Path) -> Generator[str, None, None]:
"""Generates a sequence of sections, one per slide, that will be included into the html template.""" """Generates a sequence of sections, one per slide, that will be included into the html template."""
@ -377,10 +389,7 @@ class FrameIndex(str, Enum):
class PDF(Converter): class PDF(Converter):
frame_index: FrameIndex = FrameIndex.last frame_index: FrameIndex = FrameIndex.last
resolution: PositiveFloat = 100.0 resolution: PositiveFloat = 100.0
model_config = ConfigDict(use_enum_values=True, extra="forbid")
class Config:
use_enum_values = True
extra = "forbid"
def open(self, file: Path) -> None: def open(self, file: Path) -> None:
return open_with_default(file) return open_with_default(file)
@ -432,10 +441,7 @@ class PowerPoint(Converter):
height: PositiveInt = 720 height: PositiveInt = 720
auto_play_media: bool = True auto_play_media: bool = True
poster_frame_image: Optional[FilePath] = None poster_frame_image: Optional[FilePath] = None
model_config = ConfigDict(use_enum_values=True, extra="forbid")
class Config:
use_enum_values = True
extra = "forbid"
def open(self, file: Path) -> None: def open(self, file: Path) -> None:
return open_with_default(file) return open_with_default(file)

View File

@ -11,7 +11,7 @@ import cv2
import numpy as np import numpy as np
from click import Context, Parameter from click import Context, Parameter
from pydantic import ValidationError from pydantic import ValidationError
from pydantic.color import Color from pydantic_extra_types.color import Color
from PySide6.QtCore import Qt, QThread, Signal, Slot from PySide6.QtCore import Qt, QThread, Signal, Slot
from PySide6.QtGui import QCloseEvent, QIcon, QImage, QKeyEvent, QPixmap, QResizeEvent from PySide6.QtGui import QCloseEvent, QIcon, QImage, QKeyEvent, QPixmap, QResizeEvent
from PySide6.QtWidgets import QApplication, QGridLayout, QLabel, QWidget from PySide6.QtWidgets import QApplication, QGridLayout, QLabel, QWidget

View File

@ -333,7 +333,7 @@ class Slide(Scene): # type:ignore
files=files, files=files,
resolution=self.__resolution, resolution=self.__resolution,
background_color=self.__background_color, background_color=self.__background_color,
).json(indent=2) ).model_dump_json(indent=2)
) )
logger.info( logger.info(

645
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -54,7 +54,8 @@ manimgl = {version = "^1.6.1", optional = true}
numpy = "^1.19" numpy = "^1.19"
opencv-python = "^4.6.0.66" opencv-python = "^4.6.0.66"
pillow = "^9.5.0" pillow = "^9.5.0"
pydantic = "^1.10.2" pydantic = "^2.0.1"
pydantic-extra-types = "^2.0.0"
pyside6 = "^6.5.1.1" pyside6 = "^6.5.1.1"
python = ">=3.8.1,<3.12" python = ">=3.8.1,<3.12"
python-pptx = "^0.6.21" python-pptx = "^0.6.21"
@ -84,8 +85,7 @@ sphinx-copybutton = "^0.5.1"
sphinxext-opengraph = "^0.7.5" sphinxext-opengraph = "^0.7.5"
[tool.poetry.group.test.dependencies] [tool.poetry.group.test.dependencies]
manim = "^0.17.0" pytest = "^7.4.0"
manimgl = "^1.6.1"
[tool.poetry.plugins] [tool.poetry.plugins]

99
tests/test_config.py Normal file
View File

@ -0,0 +1,99 @@
import random
import string
import tempfile
from pathlib import Path
from typing import Any, Generator, List
import pytest
from pydantic import ValidationError
from manim_slides.config import (
Key,
PresentationConfig,
SlideConfig,
SlideType,
merge_basenames,
)
def random_path(
length: int = 20,
dirname: Path = Path("./media/videos/example"),
suffix: str = ".mp4",
touch: bool = False,
) -> Path:
basename = "".join(random.choices(string.ascii_letters, k=length))
filepath = dirname.joinpath(basename + suffix)
if touch:
filepath.touch()
return filepath
@pytest.fixture
def paths() -> Generator[List[Path], None, None]:
random.seed(1234)
yield [random_path() for _ in range(20)]
@pytest.fixture
def presentation_config(paths: List[Path]) -> Generator[PresentationConfig, None, None]:
dirname = Path(tempfile.mkdtemp())
files = [random_path(dirname=dirname, touch=True) for _ in range(10)]
slides = [
SlideConfig(
type=SlideType.slide,
start_animation=0,
end_animation=5,
number=1,
),
SlideConfig(
type=SlideType.loop,
start_animation=5,
end_animation=6,
number=2,
),
SlideConfig(
type=SlideType.last,
start_animation=6,
end_animation=10,
number=3,
),
]
yield PresentationConfig(
slides=slides,
files=files,
)
def test_merge_basenames(paths: List[Path]) -> None:
path = merge_basenames(paths)
assert path.suffix == paths[0].suffix
assert path.parent == paths[0].parent
class TestKey:
@pytest.mark.parametrize(("ids", "name"), [([1], None), ([1], "some key name")])
def test_valid_keys(self, ids: Any, name: Any) -> None:
_ = Key(ids=ids, name=name)
@pytest.mark.parametrize(
("ids", "name"), [([], None), ([-1], None), ([1], {"an": " invalid name"})]
)
def test_invalid_keys(self, ids: Any, name: Any) -> None:
with pytest.raises(ValidationError):
_ = Key(ids=ids, name=name)
class TestPresentationConfig:
def test_validate(self, presentation_config: PresentationConfig) -> None:
obj = presentation_config.model_dump()
_ = PresentationConfig.model_validate(obj)
def test_bump_to_json(self, presentation_config: PresentationConfig) -> None:
_ = presentation_config.model_dump_json(indent=2)

11
tests/test_convert.py Normal file
View File

@ -0,0 +1,11 @@
import pytest
from manim_slides.convert import PDF, Converter, PowerPoint, RevealJS
class TestConverter:
@pytest.mark.parametrize(
("name", "converter"), [("html", RevealJS), ("pdf", PDF), ("pptx", PowerPoint)]
)
def test_from_string(self, name: str, converter: type) -> None:
assert Converter.from_string(name) == converter