Skip to content

Commit 326bf21

Browse files
Fixed a File logging bug and some maintenance (ManimCommunity#2546)
* Fixed File logging bug Previously, the file logger was set in manim/_config/utils. That caused problems as python tried to enable file logging when other variables like media_dir, scene_name and others which were required for the naming of log_dir were not yet set. Now, we enable log_to_file in scene_file_writer where all the required directories are declared before we enable file logging. Also removed a test that was based on set_file_logger's previous behaviour where it added a suffix only when the class name was specified when calling manim. Since, we now enable file logging in scene_file_writer, we always have access to the class name regardless. * Added Typings to file_ops * Fixed typings and some warnings in _config.utils Fixed some typings to now explicitly importing the types we require. Removed one of the double declarations of dry_run. Removed an unused import and a block of commented code. * Remove rendundant check * Add suggested changes Co-authored-by: Hugues Devimeux <[email protected]>
1 parent 5d72d9c commit 326bf21

File tree

6 files changed

+50
-94
lines changed

6 files changed

+50
-94
lines changed

manim/_config/logger_utils.py

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import copy
1616
import json
1717
import logging
18-
import os
1918
import sys
2019
from typing import TYPE_CHECKING
2120

@@ -26,7 +25,7 @@
2625
from rich.theme import Theme
2726

2827
if TYPE_CHECKING:
29-
from manim._config.utils import ManimConfig
28+
from pathlib import Path
3029
HIGHLIGHTED_KEYWORDS = [ # these keywords are highlighted specially
3130
"Played",
3231
"animations",
@@ -142,7 +141,7 @@ def parse_theme(parser: configparser.ConfigParser) -> Theme:
142141
return custom_theme
143142

144143

145-
def set_file_logger(config: ManimConfig, verbosity: str) -> None:
144+
def set_file_logger(scene_name: str, module_name: str, log_dir: Path) -> None:
146145
"""Add a file handler to manim logger.
147146
148147
The path to the file is built using ``config.log_dir``.
@@ -152,28 +151,13 @@ def set_file_logger(config: ManimConfig, verbosity: str) -> None:
152151
config : :class:`ManimConfig`
153152
The global config, used to determine the log file path.
154153
155-
verbosity : :class:`str`
156-
The verbosity level of the logger.
157-
158-
Notes
159-
-----
160-
Calling this function changes the verbosity of all handlers assigned to
161-
manim logger.
162-
163154
"""
164155
# Note: The log file name will be
165156
# <name_of_animation_file>_<name_of_scene>.log, gotten from config. So it
166157
# can differ from the real name of the scene. <name_of_scene> would only
167158
# appear if scene name was provided when manim was called.
168-
scene_name_suffix = "".join(config["scene_names"])
169-
scene_file_name = os.path.basename(config["input_file"]).split(".")[0]
170-
log_file_name = (
171-
f"{scene_file_name}_{scene_name_suffix}.log"
172-
if scene_name_suffix
173-
else f"{scene_file_name}.log"
174-
)
175-
log_file_path = config.get_dir("log_dir") / log_file_name
176-
log_file_path.parent.mkdir(parents=True, exist_ok=True)
159+
log_file_name = f"{module_name}_{scene_name}.log"
160+
log_file_path = log_dir / log_file_name
177161

178162
file_handler = logging.FileHandler(log_file_path, mode="w")
179163
file_handler.setFormatter(JSONFormatter())
@@ -182,9 +166,6 @@ def set_file_logger(config: ManimConfig, verbosity: str) -> None:
182166
logger.addHandler(file_handler)
183167
logger.info("Log file will be saved in %(logpath)s", {"logpath": log_file_path})
184168

185-
config.verbosity = verbosity
186-
logger.setLevel(verbosity)
187-
188169

189170
class JSONFormatter(logging.Formatter):
190171
"""A formatter that outputs logs in a custom JSON format.

manim/_config/utils.py

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,16 @@
1919
import os
2020
import re
2121
import sys
22-
import typing
2322
from collections.abc import Mapping, MutableMapping
2423
from pathlib import Path
24+
from typing import Any, Iterable, Iterator
2525

2626
import colour
2727
import numpy as np
2828

2929
from .. import constants
3030
from ..utils.tex import TexTemplate, TexTemplateFromFile
3131
from ..utils.tex_templates import TexTemplateLibrary
32-
from .logger_utils import set_file_logger
3332

3433

3534
def config_file_paths() -> list[Path]:
@@ -302,7 +301,7 @@ def __init__(self) -> None:
302301
self._d = {k: None for k in self._OPTS}
303302

304303
# behave like a dict
305-
def __iter__(self) -> typing.Iterator[str]:
304+
def __iter__(self) -> Iterator[str]:
306305
return iter(self._d)
307306

308307
def __len__(self) -> int:
@@ -315,10 +314,10 @@ def __contains__(self, key) -> bool:
315314
except AttributeError:
316315
return False
317316

318-
def __getitem__(self, key) -> typing.Any:
317+
def __getitem__(self, key) -> Any:
319318
return getattr(self, key)
320319

321-
def __setitem__(self, key: str, val: typing.Any) -> None:
320+
def __setitem__(self, key: str, val: Any) -> None:
322321
getattr(ManimConfig, key).fset(self, val) # fset is the property's setter
323322

324323
def update(self, obj: ManimConfig | dict) -> None:
@@ -393,7 +392,7 @@ def __copy__(self) -> ManimConfig:
393392
"""See ManimConfig.copy()."""
394393
return copy.deepcopy(self)
395394

396-
def __deepcopy__(self, memo: dict[str, typing.Any]) -> ManimConfig:
395+
def __deepcopy__(self, memo: dict[str, Any]) -> ManimConfig:
397396
"""See ManimConfig.copy()."""
398397
c = ManimConfig()
399398
# Deepcopying the underlying dict is enough because all properties
@@ -403,14 +402,14 @@ def __deepcopy__(self, memo: dict[str, typing.Any]) -> ManimConfig:
403402
return c
404403

405404
# helper type-checking methods
406-
def _set_from_list(self, key: str, val: typing.Any, values: list) -> None:
405+
def _set_from_list(self, key: str, val: Any, values: list) -> None:
407406
"""Set ``key`` to ``val`` if ``val`` is contained in ``values``."""
408407
if val in values:
409408
self._d[key] = val
410409
else:
411410
raise ValueError(f"attempted to set {key} to {val}; must be in {values}")
412411

413-
def _set_boolean(self, key: str | int, val: typing.Any) -> None:
412+
def _set_boolean(self, key: str | int, val: Any) -> None:
414413
"""Set ``key`` to ``val`` if ``val`` is Boolean."""
415414
if val in [True, False]:
416415
self._d[key] = val
@@ -423,7 +422,7 @@ def _set_tuple(self, key: str, val: tuple) -> None:
423422
else:
424423
raise ValueError(f"{key} must be tuple")
425424

426-
def _set_str(self, key: str, val: typing.Any) -> None:
425+
def _set_str(self, key: str, val: Any) -> None:
427426
"""Set ``key`` to ``val`` if ``val`` is a string."""
428427
if isinstance(val, str):
429428
self._d[key] = val
@@ -830,8 +829,7 @@ def digest_file(self, filename: str) -> ManimConfig:
830829
filename,
831830
)
832831

833-
if filename:
834-
return self.digest_parser(make_config_parser(filename))
832+
return self.digest_parser(make_config_parser(filename))
835833

836834
# config options are properties
837835
preview = property(
@@ -856,19 +854,11 @@ def digest_file(self, filename: str) -> ManimConfig:
856854
doc="Whether to show progress bars while rendering animations.",
857855
)
858856

859-
@property
860-
def log_to_file(self):
861-
"""Whether to save logs to a file."""
862-
return self._d["log_to_file"]
863-
864-
@log_to_file.setter
865-
def log_to_file(self, val: str) -> None:
866-
self._set_boolean("log_to_file", val)
867-
if val:
868-
log_dir = self.get_dir("log_dir")
869-
if not os.path.exists(log_dir):
870-
os.makedirs(log_dir)
871-
set_file_logger(self, self["verbosity"])
857+
log_to_file = property(
858+
lambda self: self._d["log_to_file"],
859+
lambda self, val: self._set_boolean("log_to_file", val),
860+
doc="Whether to save logs to a file.",
861+
)
872862

873863
notify_outdated_version = property(
874864
lambda self: self._d["notify_outdated_version"],
@@ -924,12 +914,6 @@ def log_to_file(self, val: str) -> None:
924914
doc="Set to force window when using the opengl renderer",
925915
)
926916

927-
dry_run = property(
928-
lambda self: self._d["dry_run"],
929-
lambda self, val: self._set_boolean("dry_run", val),
930-
doc="Enable dry_run so that no output files are generated and window is disabled.",
931-
)
932-
933917
@property
934918
def verbosity(self):
935919
"""Logger verbosity; "DEBUG", "INFO", "WARNING", "ERROR", or "CRITICAL" (-v)."""
@@ -1610,15 +1594,15 @@ def __init__(self, c: ManimConfig) -> None:
16101594
self.__dict__["_c"] = c
16111595

16121596
# there are required by parent class Mapping to behave like a dict
1613-
def __getitem__(self, key: str | int) -> typing.Any:
1597+
def __getitem__(self, key: str | int) -> Any:
16141598
if key in self._OPTS:
16151599
return self._c[key]
16161600
elif key in self._CONSTANTS:
16171601
return self._CONSTANTS[key]
16181602
else:
16191603
raise KeyError(key)
16201604

1621-
def __iter__(self) -> typing.Iterable:
1605+
def __iter__(self) -> Iterable:
16221606
return iter(list(self._OPTS) + list(self._CONSTANTS))
16231607

16241608
def __len__(self) -> int:

manim/scene/scene_file_writer.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from manim import __version__
2020

2121
from .. import config, logger
22+
from .._config.logger_utils import set_file_logger
2223
from ..constants import FFMPEG_BIN, GIF_FILE_EXTENSION
2324
from ..utils.file_ops import (
2425
add_extension_if_not_present,
@@ -158,6 +159,12 @@ def init_output_directories(self, scene_name):
158159
),
159160
)
160161

162+
if config["log_to_file"]:
163+
log_dir = guarantee_existence(config.get_dir("log_dir"))
164+
set_file_logger(
165+
scene_name=scene_name, module_name=module_name, log_dir=log_dir
166+
)
167+
161168
def finish_last_section(self) -> None:
162169
"""Delete current section if it is empty."""
163170
if len(self.sections) and self.sections[-1].is_empty():

manim/utils/file_ops.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
import time
2525
from pathlib import Path
2626
from shutil import copyfile
27+
from typing import TYPE_CHECKING
28+
29+
if TYPE_CHECKING:
30+
from ..scene.scene_file_writer import SceneFileWriter
2731

2832
from manim import __version__, config, logger
2933

@@ -117,33 +121,35 @@ def write_to_movie() -> bool:
117121
)
118122

119123

120-
def add_extension_if_not_present(file_name, extension):
124+
def add_extension_if_not_present(file_name: Path, extension: str) -> Path:
121125
if file_name.suffix != extension:
122126
return file_name.with_suffix(extension)
123127
else:
124128
return file_name
125129

126130

127-
def add_version_before_extension(file_name):
131+
def add_version_before_extension(file_name: Path) -> Path:
128132
file_name = Path(file_name)
129133
path, name, suffix = file_name.parent, file_name.stem, file_name.suffix
130134
return Path(path, f"{name}_ManimCE_v{__version__}{suffix}")
131135

132136

133-
def guarantee_existence(path):
137+
def guarantee_existence(path: Path) -> Path:
134138
if not os.path.exists(path):
135139
os.makedirs(path)
136-
return os.path.abspath(path)
140+
return Path(os.path.abspath(path))
137141

138142

139-
def guarantee_empty_existence(path):
143+
def guarantee_empty_existence(path: Path) -> Path:
140144
if os.path.exists(path):
141145
shutil.rmtree(path)
142146
os.makedirs(path)
143-
return os.path.abspath(path)
147+
return Path(os.path.abspath(path))
144148

145149

146-
def seek_full_path_from_defaults(file_name, default_dir, extensions):
150+
def seek_full_path_from_defaults(
151+
file_name: Path, default_dir: str, extensions: str
152+
) -> Path:
147153
possible_paths = [file_name]
148154
possible_paths += [
149155
Path(default_dir) / f"{file_name}{extension}" for extension in ["", *extensions]
@@ -155,7 +161,7 @@ def seek_full_path_from_defaults(file_name, default_dir, extensions):
155161
raise OSError(error)
156162

157163

158-
def modify_atime(file_path):
164+
def modify_atime(file_path) -> None:
159165
"""Will manually change the accessed time (called `atime`) of the file, as on a lot of OS the accessed time refresh is disabled by default.
160166
161167
Parameters
@@ -188,7 +194,7 @@ def open_file(file_path, in_browser=False):
188194
sp.Popen(commands)
189195

190196

191-
def open_media_file(file_writer):
197+
def open_media_file(file_writer: SceneFileWriter) -> None:
192198
file_paths = []
193199

194200
if config["save_last_frame"]:
@@ -207,7 +213,7 @@ def open_media_file(file_writer):
207213
logger.info(f"Previewed File at: '{file_path}'")
208214

209215

210-
def get_template_names():
216+
def get_template_names() -> list[str]:
211217
"""Returns template names from the templates directory.
212218
213219
Returns
@@ -218,7 +224,7 @@ def get_template_names():
218224
return [template_name.stem for template_name in template_path.glob("*.mtp")]
219225

220226

221-
def get_template_path():
227+
def get_template_path() -> Path:
222228
"""Returns the Path of templates directory.
223229
224230
Returns
@@ -242,7 +248,9 @@ def add_import_statement(file):
242248
f.write(import_line.rstrip("\r\n") + "\n" + content)
243249

244250

245-
def copy_template_files(project_dir=Path("."), template_name="Default"):
251+
def copy_template_files(
252+
project_dir: Path = Path("."), template_name: str = "Default"
253+
) -> None:
246254
"""Copies template files from templates dir to project_dir.
247255
248256
Parameters

tests/test_config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def test_custom_dirs(tmp_path):
9898
{
9999
"media_dir": tmp_path,
100100
"save_sections": True,
101+
"log_to_file": True,
101102
"frame_rate": 15,
102103
"pixel_height": 854,
103104
"pixel_width": 480,
@@ -108,6 +109,7 @@ def test_custom_dirs(tmp_path):
108109
"images_dir": "{media_dir}/test_images",
109110
"text_dir": "{media_dir}/test_text",
110111
"tex_dir": "{media_dir}/test_tex",
112+
"log_dir": "{media_dir}/test_log",
111113
}
112114
):
113115
scene = MyScene()
@@ -131,7 +133,7 @@ def test_custom_dirs(tmp_path):
131133

132134
assert_dir_filled(os.path.join(tmp_path, "test_text"))
133135
assert_dir_filled(os.path.join(tmp_path, "test_tex"))
134-
# TODO: testing the log dir would be nice but it doesn't get generated for some reason and test crashes when setting "log_to_file" to True
136+
assert_dir_filled(os.path.join(tmp_path, "test_log"))
135137

136138

137139
def test_frame_size(tmp_path):

tests/test_logging/test_logging.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,32 +35,6 @@ def test_logging_to_file(tmp_path, python_version):
3535
assert exitcode == 0, err
3636

3737

38-
@logs_comparison(
39-
"BasicSceneLoggingTest.txt",
40-
os.path.join("logs", "basic_scenes_square_to_circle.log"),
41-
)
42-
def test_logging_when_scene_is_not_specified(tmp_path, python_version):
43-
path_basic_scene = os.path.join(
44-
"tests",
45-
"test_logging",
46-
"basic_scenes_square_to_circle.py",
47-
)
48-
command = [
49-
python_version,
50-
"-m",
51-
"manim",
52-
"-ql",
53-
"-v",
54-
"DEBUG",
55-
"--log_to_file",
56-
"--media_dir",
57-
str(tmp_path),
58-
path_basic_scene,
59-
]
60-
_, err, exitcode = capture(command)
61-
assert exitcode == 0, err
62-
63-
6438
def test_error_logging(tmp_path, python_version):
6539
path_error_scene = Path("tests/test_logging/basic_scenes_error.py")
6640

0 commit comments

Comments
 (0)