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

1
.gitignore vendored
View File

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

View File

@ -5,10 +5,10 @@ import subprocess
import tempfile
from enum import Enum
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.color import Color
from pydantic import BaseModel, FilePath, PositiveInt, field_validator, model_validator
from pydantic_extra_types.color import Color
from PySide6.QtCore import Qt
from .defaults import FFMPEG_BIN
@ -38,18 +38,19 @@ def merge_basenames(files: List[FilePath]) -> Path:
class Key(BaseModel): # type: ignore
"""Represents a list of key codes, with optionally a name."""
ids: Set[int]
ids: Set[PositiveInt]
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:
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:
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")
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]:
ids: Set[int] = set()
@ -105,19 +106,21 @@ class SlideConfig(BaseModel): # type: ignore
number: int
terminated: bool = False
@validator("start_animation", "end_animation")
@field_validator("start_animation", "end_animation")
@classmethod
def index_is_posint(cls, v: int) -> int:
if v < 0:
raise ValueError("Animation index (start or end) cannot be negative")
return v
@validator("number")
@field_validator("number")
@classmethod
def number_is_strictly_posint(cls, v: int) -> int:
if v <= 0:
raise ValueError("Slide number cannot be negative or zero")
return v
@root_validator
@model_validator(mode="before")
def start_animation_is_before_end(
cls, values: 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)
background_color: Color = "black"
@root_validator
@model_validator(mode="after")
def animation_indices_match_files(
cls, values: Dict[str, Union[List[SlideConfig], List[FilePath]]]
) -> Dict[str, Union[List[SlideConfig], List[FilePath]]]:
files: List[FilePath] = values.get("files") # type: ignore
slides: List[SlideConfig] = values.get("slides") # type: ignore
if files is None or slides is None:
return values
cls, config: "PresentationConfig"
) -> "PresentationConfig":
files = config.files
slides = config.slides
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}"
)
return values
return config
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 lxml import etree
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 . import data
@ -87,6 +96,12 @@ class Str(str):
# This fixes pickling issue on Python 3.8
__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:
"""Ensures that the string is correctly quoted."""
if self in ["true", "false", "null"]:
@ -304,10 +319,7 @@ class RevealJS(Converter):
reveal_version: str = "4.4.0"
reveal_theme: RevealTheme = RevealTheme.black
title: str = "Manim Slides"
class Config:
use_enum_values = True
extra = "forbid"
model_config = ConfigDict(use_enum_values=True, extra="forbid")
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."""
@ -377,10 +389,7 @@ class FrameIndex(str, Enum):
class PDF(Converter):
frame_index: FrameIndex = FrameIndex.last
resolution: PositiveFloat = 100.0
class Config:
use_enum_values = True
extra = "forbid"
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def open(self, file: Path) -> None:
return open_with_default(file)
@ -432,10 +441,7 @@ class PowerPoint(Converter):
height: PositiveInt = 720
auto_play_media: bool = True
poster_frame_image: Optional[FilePath] = None
class Config:
use_enum_values = True
extra = "forbid"
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def open(self, file: Path) -> None:
return open_with_default(file)

View File

@ -11,7 +11,7 @@ import cv2
import numpy as np
from click import Context, Parameter
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.QtGui import QCloseEvent, QIcon, QImage, QKeyEvent, QPixmap, QResizeEvent
from PySide6.QtWidgets import QApplication, QGridLayout, QLabel, QWidget

View File

@ -333,7 +333,7 @@ class Slide(Scene): # type:ignore
files=files,
resolution=self.__resolution,
background_color=self.__background_color,
).json(indent=2)
).model_dump_json(indent=2)
)
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"
opencv-python = "^4.6.0.66"
pillow = "^9.5.0"
pydantic = "^1.10.2"
pydantic = "^2.0.1"
pydantic-extra-types = "^2.0.0"
pyside6 = "^6.5.1.1"
python = ">=3.8.1,<3.12"
python-pptx = "^0.6.21"
@ -84,8 +85,7 @@ sphinx-copybutton = "^0.5.1"
sphinxext-opengraph = "^0.7.5"
[tool.poetry.group.test.dependencies]
manim = "^0.17.0"
manimgl = "^1.6.1"
pytest = "^7.4.0"
[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