Skip to content

Commit dc1be25

Browse files
authored
chore(lib): use pathlib.Path instead of str (#133)
* wip(lib): change os.path to pathlib.Path * chore(lib): use pathlib.Path instead of str * fix(logger): convert Path to str * chore(lint): add type hint to prevent future errors * fix(lib): correct suffix addition
1 parent 4cd433b commit dc1be25

File tree

4 files changed

+70
-84
lines changed

4 files changed

+70
-84
lines changed

manim_slides/commons.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from pathlib import Path
12
from typing import Any, Callable
23

34
import click
@@ -18,7 +19,7 @@ def config_path_option(function: F) -> F:
1819
"config_path",
1920
metavar="FILE",
2021
default=CONFIG_PATH,
21-
type=click.Path(dir_okay=False),
22+
type=click.Path(dir_okay=False, path_type=Path),
2223
help="Set path to configuration file.",
2324
show_default=True,
2425
)
@@ -73,7 +74,7 @@ def folder_path_option(function: F) -> F:
7374
"--folder",
7475
metavar="DIRECTORY",
7576
default=FOLDER_PATH,
76-
type=click.Path(exists=True, file_okay=False),
77+
type=click.Path(exists=True, file_okay=False, path_type=Path),
7778
help="Set slides folder.",
7879
show_default=True,
7980
)

manim_slides/config.py

+19-35
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,33 @@
44
import subprocess
55
import tempfile
66
from enum import Enum
7-
from typing import Callable, Dict, List, Optional, Set, Union
7+
from pathlib import Path
8+
from typing import Dict, List, Optional, Set, Union
89

9-
from pydantic import BaseModel, root_validator, validator
10+
from pydantic import BaseModel, FilePath, root_validator, validator
1011
from PySide6.QtCore import Qt
1112

1213
from .manim import FFMPEG_BIN, logger
1314

1415

15-
def merge_basenames(files: List[str]) -> str:
16+
def merge_basenames(files: List[FilePath]) -> Path:
1617
"""
1718
Merge multiple filenames by concatenating basenames.
1819
"""
1920
logger.info(f"Generating a new filename for animations: {files}")
2021

21-
dirname = os.path.dirname(files[0])
22-
_, ext = os.path.splitext(files[0])
22+
dirname = files[0].parent
23+
ext = files[0].suffix
2324

24-
basenames = (os.path.splitext(os.path.basename(file))[0] for file in files)
25+
basenames = (file.stem for file in files)
2526

2627
basenames_str = ",".join(f"{len(b)}:{b}" for b in basenames)
2728

2829
# We use hashes to prevent too-long filenames, see issue #123:
2930
# https://github.com/jeertmans/manim-slides/issues/123
3031
basename = hashlib.sha256(basenames_str.encode()).hexdigest()
3132

32-
return os.path.join(dirname, basename + ext)
33+
return dirname / (basename + ext)
3334

3435

3536
class Key(BaseModel): # type: ignore
@@ -146,24 +147,12 @@ def slides_slice(self) -> slice:
146147

147148
class PresentationConfig(BaseModel): # type: ignore
148149
slides: List[SlideConfig]
149-
files: List[str]
150-
151-
@validator("files", pre=True, each_item=True)
152-
def is_file_and_exists(cls, v: str) -> str:
153-
if not os.path.exists(v):
154-
raise ValueError(
155-
f"Animation file {v} does not exist. Are you in the right directory?"
156-
)
157-
158-
if not os.path.isfile(v):
159-
raise ValueError(f"Animation file {v} is not a file")
160-
161-
return v
150+
files: List[FilePath]
162151

163152
@root_validator
164153
def animation_indices_match_files(
165-
cls, values: Dict[str, Union[List[SlideConfig], List[str]]]
166-
) -> Dict[str, Union[List[SlideConfig], List[str]]]:
154+
cls, values: Dict[str, Union[List[SlideConfig], List[FilePath]]]
155+
) -> Dict[str, Union[List[SlideConfig], List[FilePath]]]:
167156
files = values.get("files")
168157
slides = values.get("slides")
169158

@@ -180,26 +169,21 @@ def animation_indices_match_files(
180169

181170
return values
182171

183-
def move_to(self, dest: str, copy: bool = True) -> "PresentationConfig":
172+
def copy_to(self, dest: Path) -> "PresentationConfig":
184173
"""
185-
Moves (or copy) the files to a given directory.
174+
Copy the files to a given directory.
186175
"""
187-
copy_func: Callable[[str, str], None] = shutil.copy
188-
move_func: Callable[[str, str], None] = shutil.move
189-
move = copy_func if copy else move_func
190-
191176
n = len(self.files)
192177
for i in range(n):
193178
file = self.files[i]
194-
basename = os.path.basename(file)
195-
dest_path = os.path.join(dest, basename)
179+
dest_path = dest / self.files[i].name
196180
logger.debug(f"Moving / copying {file} to {dest_path}")
197-
move(file, dest_path)
181+
shutil.copy(file, dest_path)
198182
self.files[i] = dest_path
199183

200184
return self
201185

202-
def concat_animations(self, dest: Optional[str] = None) -> "PresentationConfig":
186+
def concat_animations(self, dest: Optional[Path] = None) -> "PresentationConfig":
203187
"""
204188
Concatenate animations such that each slide contains one animation.
205189
"""
@@ -216,7 +200,7 @@ def concat_animations(self, dest: Optional[str] = None) -> "PresentationConfig":
216200
f.writelines(f"file '{os.path.abspath(path)}'\n" for path in files)
217201
f.close()
218202

219-
command = [
203+
command: List[str] = [
220204
FFMPEG_BIN,
221205
"-f",
222206
"concat",
@@ -226,7 +210,7 @@ def concat_animations(self, dest: Optional[str] = None) -> "PresentationConfig":
226210
f.name,
227211
"-c",
228212
"copy",
229-
dest_path,
213+
str(dest_path),
230214
"-y",
231215
]
232216
logger.debug(" ".join(command))
@@ -252,7 +236,7 @@ def concat_animations(self, dest: Optional[str] = None) -> "PresentationConfig":
252236
self.files = dest_paths
253237

254238
if dest:
255-
return self.move_to(dest)
239+
return self.copy_to(dest)
256240

257241
return self
258242

manim_slides/convert.py

+21-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import webbrowser
33
from enum import Enum
4+
from pathlib import Path
45
from typing import Any, Callable, Dict, Generator, List, Optional, Type, Union
56

67
import click
@@ -35,7 +36,7 @@ class Converter(BaseModel): # type: ignore
3536
assets_dir: str = "{basename}_assets"
3637
template: Optional[str] = None
3738

38-
def convert_to(self, dest: str) -> None:
39+
def convert_to(self, dest: Path) -> None:
3940
"""Converts self, i.e., a list of presentations, into a given format."""
4041
raise NotImplementedError
4142

@@ -45,7 +46,7 @@ def load_template(self) -> str:
4546
An empty string is returned if no template is used."""
4647
return ""
4748

48-
def open(self, file: str) -> bool:
49+
def open(self, file: Path) -> bool:
4950
"""Opens a file, generated with converter, using appropriate application."""
5051
raise NotImplementedError
5152

@@ -285,12 +286,12 @@ class Config:
285286
use_enum_values = True
286287
extra = "forbid"
287288

288-
def get_sections_iter(self) -> Generator[str, None, None]:
289+
def get_sections_iter(self, assets_dir: Path) -> Generator[str, None, None]:
289290
"""Generates a sequence of sections, one per slide, that will be included into the html template."""
290291
for presentation_config in self.presentation_configs:
291292
for slide_config in presentation_config.slides:
292293
file = presentation_config.files[slide_config.start_animation]
293-
file = os.path.join(self.assets_dir, os.path.basename(file))
294+
file = assets_dir / file.name
294295

295296
# TODO: document this
296297
# Videos are muted because, otherwise, the first slide never plays correctly.
@@ -312,26 +313,27 @@ def load_template(self) -> str:
312313
__name__, "data/revealjs_template.html"
313314
).decode()
314315

315-
def open(self, file: str) -> bool:
316-
return webbrowser.open(file)
316+
def open(self, file: Path) -> bool:
317+
return webbrowser.open(file.absolute().as_uri())
317318

318-
def convert_to(self, dest: str) -> None:
319+
def convert_to(self, dest: Path) -> None:
319320
"""Converts this configuration into a RevealJS HTML presentation, saved to DEST."""
320-
dirname = os.path.dirname(dest)
321-
basename, ext = os.path.splitext(os.path.basename(dest))
321+
dirname = dest.parent
322+
basename = dest.stem
323+
ext = dest.suffix
322324

323-
self.assets_dir = self.assets_dir.format(
324-
dirname=dirname, basename=basename, ext=ext
325+
assets_dir = Path(
326+
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
325327
)
326-
full_assets_dir = os.path.join(dirname, self.assets_dir)
328+
full_assets_dir = dirname / assets_dir
327329

328330
os.makedirs(full_assets_dir, exist_ok=True)
329331

330332
for presentation_config in self.presentation_configs:
331-
presentation_config.concat_animations().move_to(full_assets_dir)
333+
presentation_config.concat_animations().copy_to(full_assets_dir)
332334

333335
with open(dest, "w") as f:
334-
sections = "".join(self.get_sections_iter())
336+
sections = "".join(self.get_sections_iter(full_assets_dir))
335337

336338
revealjs_template = self.load_template()
337339
content = revealjs_template.format(sections=sections, **self.dict())
@@ -396,7 +398,7 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None:
396398
@click.command()
397399
@click.argument("scenes", nargs=-1)
398400
@folder_path_option
399-
@click.argument("dest")
401+
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
400402
@click.option(
401403
"--to",
402404
type=click.Choice(["html"], case_sensitive=False),
@@ -423,21 +425,21 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None:
423425
"--use-template",
424426
"template",
425427
metavar="FILE",
426-
type=click.Path(exists=True, dir_okay=False),
428+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
427429
help="Use the template given by FILE instead of default one. To echo the default template, use `--show-template`.",
428430
)
429431
@show_template_option
430432
@show_config_options
431433
@verbosity_option
432434
def convert(
433435
scenes: List[str],
434-
folder: str,
435-
dest: str,
436+
folder: Path,
437+
dest: Path,
436438
to: str,
437439
open_result: bool,
438440
force: bool,
439441
config_options: Dict[str, str],
440-
template: Optional[str],
442+
template: Optional[Path],
441443
) -> None:
442444
"""
443445
Convert SCENE(s) into a given format and writes the result in DEST.

0 commit comments

Comments
 (0)