Skip to content

Commit 50c504f

Browse files
huguesdevimeuxHugues DevimeuxPgBielleotrs
authored
Refactored logs and added log-to-json feature (removed the old log-to-file feature), added logging test utils. (#403)
* refactored logging, and logging to file feature. * cleaned up old tests * Update tests/test_logging/test_logging.py Co-authored-by: Hugues Devimeux <[email protected]> Co-authored-by: Pg Biel <[email protected]> Co-authored-by: Leo Torres <[email protected]>
1 parent e6e8cd9 commit 50c504f

File tree

16 files changed

+221
-162
lines changed

16 files changed

+221
-162
lines changed

docs/source/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ Of interest to developers
8989
#. Added autogenerated documentation with sphinx and autodoc/autosummary [WIP]
9090
#. Made manim internally use relative imports
9191
#. Since the introduction of the :code:`TexTemplate` class, the files :code:`tex_template.tex` and :code:`ctex_template.tex` have been removed
92+
#. Added logging tests tools.
93+
#. Added ability to save logs in json
9294
#. Move to Poetry.
9395

9496
Other Changes

manim/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .scene.scene import Scene
1515
from .utils.sounds import play_error_sound, play_finish_sound
1616
from .utils.file_ops import open_file as open_media_file
17+
from . import constants
1718

1819

1920
def open_file_if_needed(file_writer):

manim/config/config.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
from .. import constants
1919
from .config_utils import _run_config, _init_dirs, _from_command_line
2020

21-
from .logger import logger
21+
from .logger import set_rich_logger, set_file_logger, logger
2222
from ..utils.tex import TexTemplate, TexTemplateFromFile
2323

24+
__all__ = ["file_writer_config", "config", "camera_config", "tempconfig"]
25+
2426

2527
config = None
2628

@@ -150,3 +152,14 @@ def _parse_config(config_parser, args):
150152
_init_dirs(file_writer_config)
151153
config = _parse_config(config_parser, args)
152154
camera_config = config
155+
156+
# Set the different loggers
157+
set_rich_logger(config_parser["logger"], file_writer_config["verbosity"])
158+
if file_writer_config["log_to_file"]:
159+
# IMPORTANT note about file name : The log file name will be the scene_name get from the args (contained in file_writer_config). So it can differ from the real name of the scene.
160+
log_file_path = os.path.join(
161+
file_writer_config["log_dir"],
162+
"".join(file_writer_config["scene_names"]) + ".log",
163+
)
164+
set_file_logger(log_file_path)
165+
logger.info("Log file wil be saved in %(logpath)s", {"logpath": log_file_path})

manim/config/config_utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,15 @@ def _parse_file_writer_config(config_parser, args):
6969
default.getboolean(boolean_opt) if attr is None else attr
7070
)
7171
# for str_opt in ['media_dir', 'video_dir', 'tex_dir', 'text_dir']:
72-
for str_opt in ["media_dir", "log_dir"]:
72+
for str_opt in ["media_dir"]:
7373
attr = getattr(args, str_opt)
7474
fw_config[str_opt] = os.path.relpath(default[str_opt]) if attr is None else attr
75+
attr = getattr(args, "log_dir")
76+
fw_config["log_dir"] = (
77+
os.path.join(fw_config["media_dir"], default["log_dir"])
78+
if attr is None
79+
else attr
80+
)
7581
dir_names = {
7682
"video_dir": "videos",
7783
"images_dir": "images",

manim/config/default.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ upto_animation_number = -1
7575
# as well as Tex and texts.
7676
media_dir = ./media
7777

78-
# --log_dir
79-
log_dir = %(media_dir)s/logs
78+
# --log_dir (by default "/logs", that will be put inside the media dir)
79+
log_dir = logs
8080

8181
# If the -t (--transparent) flag is used, these will be replaced with the
8282
# values specified in the [TRANSPARENT] section later in this file.

manim/config/logger.py

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,41 @@
1818
from rich.theme import Theme
1919
from rich import print as printf
2020
from rich import errors, color
21+
import json
22+
import copy
2123

22-
from .config_utils import _run_config
2324

25+
class JSONFormatter(logging.Formatter):
26+
"""Subclass of `:class:`logging.Formatter`, to build our own format of the logs (JSON)."""
2427

25-
def parse_theme(fp):
26-
config_parser.read(fp)
27-
theme = dict(config_parser["logger"])
28-
# replaces `_` by `.` as rich understands it
28+
def format(self, record):
29+
record_c = copy.deepcopy(record)
30+
if record_c.args:
31+
for arg in record_c.args:
32+
record_c.args[arg] = "<>"
33+
return json.dumps(
34+
{
35+
"levelname": record_c.levelname,
36+
"module": record_c.module,
37+
"message": super().format(record_c),
38+
}
39+
)
40+
41+
42+
def _parse_theme(config_logger):
2943
theme = dict(
30-
zip([key.replace("_", ".") for key in theme.keys()], list(theme.values()))
44+
zip(
45+
[key.replace("_", ".") for key in config_logger.keys()],
46+
list(config_logger.values()),
47+
)
3148
)
3249

3350
theme["log.width"] = None if theme["log.width"] == "-1" else int(theme["log.width"])
3451

3552
theme["log.height"] = (
3653
None if theme["log.height"] == "-1" else int(theme["log.height"])
3754
)
38-
theme["log.timestamps"] = config_parser["logger"].getboolean("log.timestamps")
55+
theme["log.timestamps"] = False
3956
try:
4057
customTheme = Theme(
4158
{
@@ -49,51 +66,54 @@ def parse_theme(fp):
4966
printf(
5067
"[logging.level.error]It seems your colour configuration couldn't be parsed. Loading the default color configuration...[/logging.level.error]"
5168
)
52-
return customTheme, theme
53-
54-
55-
config_items = _run_config()
56-
config_parser, successfully_read_files = config_items[1], config_items[-1]
57-
try:
58-
customTheme, themedict = parse_theme(successfully_read_files)
59-
console = Console(
60-
theme=customTheme,
61-
record=True,
62-
height=themedict["log.height"],
63-
width=themedict["log.width"],
64-
)
65-
except KeyError:
66-
console = Console(record=True)
67-
printf(
68-
"[logging.level.warning]No cfg file found, creating one in "
69-
+ successfully_read_files[0]
70-
+ " [/logging.level.warning]"
69+
return customTheme
70+
71+
72+
def set_rich_logger(config_logger, verbosity):
73+
"""Will set the RichHandler of the logger.
74+
75+
Parameter
76+
----------
77+
config_logger :class:
78+
Config object of the logger.
79+
"""
80+
theme = _parse_theme(config_logger)
81+
global console
82+
console = Console(theme=theme)
83+
# These keywords Are Highlighted specially.
84+
RichHandler.KEYWORDS = [
85+
"Played",
86+
"animations",
87+
"scene",
88+
"Reading",
89+
"Writing",
90+
"script",
91+
"arguments",
92+
"Invalid",
93+
"Aborting",
94+
"module",
95+
"File",
96+
"Rendering",
97+
"Rendered",
98+
]
99+
rich_handler = RichHandler(
100+
console=console, show_time=config_logger.getboolean("log_timestamps")
71101
)
102+
global logger
103+
rich_handler.setLevel(verbosity)
104+
logger.addHandler(rich_handler)
105+
106+
107+
def set_file_logger(log_file_path):
108+
file_handler = logging.FileHandler(log_file_path, mode="w")
109+
file_handler.setFormatter(JSONFormatter())
110+
global logger
111+
logger.addHandler(file_handler)
112+
72113

73-
# These keywords Are Highlighted specially.
74-
RichHandler.KEYWORDS = [
75-
"Played",
76-
"animations",
77-
"scene",
78-
"Reading",
79-
"Writing",
80-
"script",
81-
"arguments",
82-
"Invalid",
83-
"Aborting",
84-
"module",
85-
"File",
86-
"Rendering",
87-
"Rendered",
88-
]
89-
logging.basicConfig(
90-
level="NOTSET",
91-
format="%(message)s",
92-
datefmt="[%X]",
93-
handlers=[RichHandler(console=console, show_time=themedict["log.timestamps"])],
94-
)
95-
96-
logger = logging.getLogger("rich")
114+
logger = logging.getLogger("manim")
115+
# The console is set to None as it will be changed by set_rich_logger.
116+
console = None
97117

98118
# TODO : This is only temporary to keep the terminal output clean when working with ImageMobject and matplotlib plots
99119
logging.getLogger("PIL").setLevel(logging.INFO)

manim/scene/scene.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,8 @@ def wrapper(self, *args, **kwargs):
816816
self.play_hashes_list.append(hash_play)
817817
if self.file_writer.is_already_cached(hash_play):
818818
logger.info(
819-
f"Animation {self.num_plays} : Using cached data (hash : {hash_play})"
819+
f"Animation {self.num_plays} : Using cached data (hash : %(hash_play)s)",
820+
{"hash_play": hash_play},
820821
)
821822
file_writer_config["skip_animations"] = True
822823
else:

manim/scene/scene_file_writer.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,8 @@ def close_movie_pipe(self):
454454
self.partial_movie_file_path,
455455
)
456456
logger.info(
457-
f"Animation {self.scene.num_plays} : Partial movie file written in {self.partial_movie_file_path}"
457+
f"Animation {self.scene.num_plays} : Partial movie file written in %(path)s",
458+
{"path": {self.partial_movie_file_path}},
458459
)
459460

460461
def is_already_cached(self, hash_invocation):
@@ -616,21 +617,12 @@ def flush_cache_directory(self):
616617
for f in cached_partial_movies:
617618
os.remove(f)
618619
logger.info(
619-
f"Cache flushed. {len(cached_partial_movies)} file(s) deleted in {self.partial_movie_directory}."
620+
f"Cache flushed. {len(cached_partial_movies)} file(s) deleted in %(par_dir)s.",
621+
{"par_dir": self.partial_movie_directory},
620622
)
621623

622624
def print_file_ready_message(self, file_path):
623625
"""
624626
Prints the "File Ready" message to STDOUT.
625627
"""
626-
logger.info("\nFile ready at {}\n".format(file_path))
627-
628-
if file_writer_config["log_to_file"]:
629-
self.write_log()
630-
631-
def write_log(self):
632-
log_file_path = os.path.join(
633-
file_writer_config["log_dir"], f"{self.get_default_scene_name()}.log"
634-
)
635-
console.save_text(log_file_path)
636-
logger.info("Log written to {}\n".format(log_file_path))
628+
logger.info("\nFile ready at %(file_path)s\n", {"file_path": file_path})

manim/utils/hashing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ def get_hash_from_play_call(
244244
for json_val in [camera_json, animations_list_json, current_mobjects_list_json]
245245
]
246246
t_end = perf_counter()
247-
logger.debug("Hashing done in {:.5f} s.".format(t_end - t_start))
247+
logger.debug("Hashing done in %(time)s s.", {"time": str(t_end - t_start)[:8]})
248248
# This will reset ALREADY_PROCESSED_ID as all the hashing processus is finished.
249249
ALREADY_PROCESSED_ID = {}
250250
return "{}_{}_{}".format(hash_camera, hash_animations, hash_current_mobjects)
@@ -291,7 +291,7 @@ def get_hash_from_wait_call(
291291
# This will reset ALREADY_PROCESSED_ID as all the hashing processus is finished.
292292
ALREADY_PROCESSED_ID = {}
293293
t_end = perf_counter()
294-
logger.debug("Hashing done in {:.5f} s.".format(t_end - t_start))
294+
logger.debug("Hashing done in %(time)s s.", {"time": str(t_end - t_start)[:8]})
295295
return "{}_{}{}_{}".format(
296296
hash_camera,
297297
str(wait_time).replace(".", "-"),
@@ -300,7 +300,7 @@ def get_hash_from_wait_call(
300300
)
301301
ALREADY_PROCESSED_ID = {}
302302
t_end = perf_counter()
303-
logger.debug("Hashing done in {:.5f} s.".format(t_end - t_start))
303+
logger.debug("Hashing done in %(time)s s.", {"time": str(t_end - t_start)[:8]})
304304
return "{}_{}_{}".format(
305305
hash_camera, str(wait_time).replace(".", "-"), hash_current_mobjects
306306
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{"levelname": "INFO", "module": "config", "message": "Log file wil be saved in <>"}
2+
{"levelname": "DEBUG", "module": "hashing", "message": "Hashing ..."}
3+
{"levelname": "DEBUG", "module": "hashing", "message": "Hashing done in <> s."}
4+
{"levelname": "INFO", "module": "scene_file_writer", "message": "Animation 0 : Partial movie file written in <>"}
5+
{"levelname": "INFO", "module": "scene_file_writer", "message": "\nFile ready at <>\n"}
6+
{"levelname": "INFO", "module": "scene", "message": "Rendered SquareToCircle\nPlayed 1 animations"}

0 commit comments

Comments
 (0)