Repository: ManimCommunity/manim
Pull request: Add logger and replace print calls with appropriate logging functions
This commit adds a logger.py file and also replaces all `print` calls with logging functions like `logger.info`, `logger.warning`, `logger.error` etc.

Also adds rich as a dependency.



Problem statement:
Use a logging library
The first step in this task is to replace print calls with a logging library. The next step is to add logging calls to places that don't have logging yet.

@eulertour mentions both Loguru and the stdlib logging library as options. I'm partial to stdlib logging.

Hints from issue discussion:
I’m too. It would save us from adding other dependencies.
Then do we keep the progressbar ?
I think the progressbar can definitely stay, I think the progressbar is a good UI addition for users creating their videos. The logging library would be a catch-all ranging from info to users as well as debugging information for developers.
+1 for stdlib package. If anyone has more detailed input on this, though, please let us know
On discord we've settled for using [rich](https://github.com/willmcgugan/rich). I'll be pushing the changes soon.
Hi @kilacoda. Apparently, @huguesdevimeux, @PgBiel, and I are partial to the logging lib from stdlib. Can you please briefly summarize the reasons why rich was settled on?
Well, for one Rich allows us to format messages with more flexibility, such as adding colors to specific log elements (errors, warnings etc.), and makes things in general look better. It also provides its own markup language which can be used to format stuff like the Animation numbers. 

Rich has more features too such as progress bars, tables, better tracebacks etc. which may be used later on if we want to say, remove `tqdm`.

TL;DR It looks good and seemed cool to the other guys so that's why.
![image](https://user-images.githubusercontent.com/65204531/82442875-1d0bd580-9abe-11ea-9e54-0ebc36a9fa4f.png)
Here's a little example of rich I've used in my manim renderer GUI. Looks better right?
I do like the idea of handling logging and the progressbar from a single library.

I'm very very wary of using Yet Another Markup Language... but it seems it also supports markdown which is great.

[Here](https://github.com/willmcgugan/rich) is a link for reference.
Imo the usage of `rich` should be optional. We can easily implement colors by ourselves, without requiring yet another dep. We gotta keep those dep numbers down as much as possible. I'm not gonna lie, it looks like a cool lib, but we could do this (or most of it) using only stdlib, and it's not extremely necessary regardless..
I agree with @PgBiel. Adding new dep is a bad idea. 
But is implementing colors _that_ simple? Isn't that highly platform-dependent?

Commit message: Add logger and replace print calls with appropriate logging functions

This commit adds a logger.py file and also replaces all `print` calls with logging functions like `logger.info`, `logger.warning`, `logger.error` etc.

Also adds rich as a dependency.

Patch to review:
diff --git a/manim/__init__.py b/manim/__init__.py
index 1185c9c3b0..4540a58256 100644
--- a/manim/__init__.py
+++ b/manim/__init__.py
@@ -2,8 +2,7 @@
 from . import config
 from . import constants
 
-
-def main():
+def main():    
     args = config.parse_cli()
     cfg = config.get_configuration(args)
     constants.initialize_directories(cfg)
diff --git a/manim/camera/camera.py b/manim/camera/camera.py
index a0985388ff..5a76684b98 100644
--- a/manim/camera/camera.py
+++ b/manim/camera/camera.py
@@ -10,6 +10,7 @@
 import numpy as np
 
 from ..constants import *
+from ..logger import logger
 from ..mobject.types.image_mobject import AbstractImageMobject
 from ..mobject.mobject import Mobject
 from ..mobject.types.point_cloud_mobject import PMobject
@@ -24,7 +25,6 @@
 from ..utils.space_ops import angle_of_vector
 from ..utils.space_ops import get_norm
 
-
 class Camera(object):
     """
     Base Camera class.
@@ -348,14 +348,14 @@ def make_background_from_func(self, coords_to_colors_func):
             The pixel array which can then be passed to set_background.
         """
 
-        print("Starting set_background; for reference, the current time is ", time.strftime("%H:%M:%S"))
+        logger.info("Starting set_background; for reference, the current time is ", time.strftime("%H:%M:%S"))
         coords = self.get_coords_of_all_pixels()
         new_background = np.apply_along_axis(
             coords_to_colors_func,
             2,
             coords
         )
-        print("Ending set_background; for reference, the current time is ", time.strftime("%H:%M:%S"))
+        logger.info("Ending set_background; for reference, the current time is ", time.strftime("%H:%M:%S"))
 
         return self.convert_pixel_array(new_background, convert_from_floats=True)
 
diff --git a/manim/config.py b/manim/config.py
index 1fb2e11ced..972d70f660 100644
--- a/manim/config.py
+++ b/manim/config.py
@@ -5,7 +5,9 @@
 import types
 
 from . import constants
+from .logger import logger
 
+ 
 
 def parse_cli():
     try:
@@ -133,7 +135,7 @@ def parse_cli():
         )
         return parser.parse_args()
     except argparse.ArgumentError as err:
-        print(str(err))
+        logger.error(str(err))
         sys.exit(2)
 
 
@@ -229,8 +231,8 @@ def get_camera_configuration(args):
         try:
             camera_config["background_color"] = colour.Color(args.color)
         except AttributeError as err:
-            print("Please use a valid color")
-            print(err)
+            logger.warning("Please use a valid color")
+            logger.error(err)
             sys.exit(2)
 
     # If rendering a transparent image/move, make sure the
diff --git a/manim/constants.py b/manim/constants.py
index 9ae2163af5..0a4f19d20d 100644
--- a/manim/constants.py
+++ b/manim/constants.py
@@ -1,5 +1,6 @@
 import numpy as np
 import os
+from .logger import logger
 
 MEDIA_DIR = ""
 VIDEO_DIR = ""
@@ -7,7 +8,6 @@
 TEX_DIR = ""
 TEXT_DIR = ""
 
-
 def initialize_directories(config):
     global MEDIA_DIR
     global VIDEO_DIR
@@ -27,13 +27,13 @@ def initialize_directories(config):
             )
         if not os.path.isdir(MEDIA_DIR):
             MEDIA_DIR = "./media"
-        print(
+        logger.info(
             f"Media will be written to {MEDIA_DIR + os.sep}. You can change "
             "this behavior with the --media_dir flag."
         )
     else:
         if config["media_dir"]:
-            print(
+            logger.warning(
                 "Ignoring --media_dir, since both --tex_dir and a video "
                 "directory were both passed"
             )
@@ -53,7 +53,6 @@ def initialize_directories(config):
             os.makedirs(folder)
 
 NOT_SETTING_FONT_MSG='''
-Warning:
 You haven't set font.
 If you are not using English, this may cause text rendering problem.
 You set font like:
diff --git a/manim/extract_scene.py b/manim/extract_scene.py
index 27f9e1fbd5..b6feec706c 100644
--- a/manim/extract_scene.py
+++ b/manim/extract_scene.py
@@ -11,6 +11,7 @@
 from .utils.sounds import play_error_sound
 from .utils.sounds import play_finish_sound
 from . import constants
+from .logger import logger
 
 
 def open_file_if_needed(file_writer, **config):
@@ -83,7 +84,7 @@ def prompt_user_for_choice(scene_classes):
             for num_str in user_input.split(",")
         ]
     except KeyError:
-        print(constants.INVALID_NUMBER_MESSAGE)
+        logger.error(constants.INVALID_NUMBER_MESSAGE)
         sys.exit(2)
         user_input = input(constants.CHOOSE_NUMBER_MESSAGE)
         return [
@@ -96,7 +97,7 @@ def prompt_user_for_choice(scene_classes):
 
 def get_scenes_to_render(scene_classes, config):
     if len(scene_classes) == 0:
-        print(constants.NO_SCENE_MESSAGE)
+        logger.error(constants.NO_SCENE_MESSAGE)
         return []
     if config["write_all"]:
         return scene_classes
@@ -109,7 +110,7 @@ def get_scenes_to_render(scene_classes, config):
                 found = True
                 break
         if not found and (scene_name != ""):
-            print(
+            logger.error(
                 constants.SCENE_NOT_FOUND_MESSAGE.format(
                     scene_name
                 ),
@@ -141,7 +142,7 @@ def get_module(file_name):
             exec(code, module.__dict__)
             return module
         except Exception as e:
-            print(f"Failed to render scene: {str(e)}")
+            logger.error(f"Failed to render scene: {str(e)}")
             sys.exit(2)
     else:
         module_name = file_name.replace(os.sep, ".").replace(".py", "")
diff --git a/manim/logger.py b/manim/logger.py
new file mode 100644
index 0000000000..4e4e05b4e4
--- /dev/null
+++ b/manim/logger.py
@@ -0,0 +1,12 @@
+import logging
+from rich.logging import RichHandler
+
+## Setting config for logger.
+logging.basicConfig(
+    level="NOTSET",
+    format="%(message)s",
+    datefmt="[%X]",
+    handlers=[RichHandler()]
+)
+
+logger = logging.getLogger("rich")
\ No newline at end of file
diff --git a/manim/mobject/svg/text_mobject.py b/manim/mobject/svg/text_mobject.py
index 9736c2d008..1e56ae49db 100644
--- a/manim/mobject/svg/text_mobject.py
+++ b/manim/mobject/svg/text_mobject.py
@@ -5,6 +5,7 @@
 import cairo
 from ...constants import *
 from ...container.container import Container
+from ...logger import logger
 from ...mobject.geometry import Dot, Rectangle
 from ...mobject.svg.svg_mobject import SVGMobject
 from ...mobject.types.vectorized_mobject import VGroup
@@ -287,7 +288,7 @@ def text2svg(self):
 
         if self.font == '':
             if NOT_SETTING_FONT_MSG != '':
-                print(NOT_SETTING_FONT_MSG)
+                logger.warning(NOT_SETTING_FONT_MSG)
 
         dir_name = TEXT_DIR
         hash_name = self.text2hash()
diff --git a/manim/mobject/vector_field.py b/manim/mobject/vector_field.py
index 50a5bd82ec..774b753eee 100644
--- a/manim/mobject/vector_field.py
+++ b/manim/mobject/vector_field.py
@@ -5,7 +5,7 @@
 import random
 
 from ..constants import *
-
+from ..logger import logger
 from ..animation.composition import AnimationGroup
 from ..animation.indication import ShowPassingFlash
 from ..mobject.geometry import Vector
@@ -85,7 +85,7 @@ def get_color_field_image_file(scalar_func,
     file_name = "%d.png" % func_hash
     full_path = os.path.join(RASTER_IMAGE_DIR, file_name)
     if not os.path.exists(full_path):
-        print("Rendering color field image " + str(func_hash))
+        logger.info("Rendering color field image " + str(func_hash))
         rgb_gradient_func = get_rgb_gradient_function(
             min_value=min_value,
             max_value=max_value,
diff --git a/manim/scene/scene.py b/manim/scene/scene.py
index 1dac19dfdf..ce24579c48 100644
--- a/manim/scene/scene.py
+++ b/manim/scene/scene.py
@@ -11,6 +11,7 @@
 from ..camera.camera import Camera
 from ..constants import *
 from ..container.container import Container
+from ..logger import logger
 from ..mobject.mobject import Mobject
 from ..scene.scene_file_writer import SceneFileWriter
 from ..utils.iterables import list_update
@@ -110,7 +111,7 @@ def print_end_message(self):
         Used internally to print the number of
         animations played after the scene ends.
         """
-        print("Played {} animations".format(self.num_plays))
+        logger.info("Played {} animations".format(self.num_plays))
 
     def set_variables_as_attrs(self, *objects, **newly_named_objects):
         """
diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py
index d68dc33cb9..9cbc61cb59 100644
--- a/manim/scene/scene_file_writer.py
+++ b/manim/scene/scene_file_writer.py
@@ -14,6 +14,7 @@
 from ..constants import STREAMING_PROTOCOL
 from ..constants import VIDEO_DIR
 from ..constants import VIDEO_OUTPUT_DIR
+from ..logger import logger
 from ..utils.config_ops import digest_config
 from ..utils.file_ops import guarantee_existence
 from ..utils.file_ops import add_extension_if_not_present
@@ -473,7 +474,7 @@ def combine_movie_files(self):
             **kwargs
         )
         if len(partial_movie_files) == 0:
-            print("No animations in this scene")
+            logger.error("No animations in this scene")
             return
 
         # Write a file partial_file_list.txt containing all
@@ -542,4 +543,4 @@ def print_file_ready_message(self, file_path):
         """
         Prints the "File Ready" message to STDOUT.
         """
-        print("\nFile ready at {}\n".format(file_path))
+        logger.info("\nFile ready at {}\n".format(file_path))
diff --git a/manim/scene/scene_from_video.py b/manim/scene/scene_from_video.py
index ba3e6562ec..170c6f0162 100644
--- a/manim/scene/scene_from_video.py
+++ b/manim/scene/scene_from_video.py
@@ -2,6 +2,7 @@
 import cv2
 
 from ..scene.scene import Scene
+from ..logger import logger
 
 
 # TODO, is this depricated?
@@ -24,7 +25,7 @@ def construct(self, file_name,
             start_frame, end_frame = [fps * t for t in time_range]
 
         frame_count = end_frame - start_frame
-        print("Reading in " + file_name + "...")
+        logger.info("Reading in " + file_name + "...")
         for count in show_progress(list(range(start_frame, end_frame + 1))):
             returned, frame = cap.read()
             if not returned:
diff --git a/manim/utils/tex_file_writing.py b/manim/utils/tex_file_writing.py
index 4e166b74a4..d781f1797b 100644
--- a/manim/utils/tex_file_writing.py
+++ b/manim/utils/tex_file_writing.py
@@ -6,6 +6,7 @@
 from ..constants import TEX_TEXT_TO_REPLACE
 from ..constants import TEX_USE_CTEX
 from ..constants import TEX_DIR
+from ..logger import logger
 
 
 def tex_hash(expression, template_tex_file_body):
@@ -28,7 +29,7 @@ def generate_tex_file(expression, template_tex_file_body):
         tex_hash(expression, template_tex_file_body)
     ) + ".tex"
     if not os.path.exists(result):
-        print("Writing \"%s\" to %s" % (
+        logger.info("Writing \"%s\" to %s" % (
             "".join(expression), result
         ))
         new_body = template_tex_file_body.replace(
diff --git a/requirements.txt b/requirements.txt
index 9b54f76ae4..7dec25c9b3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,4 +9,5 @@ opencv-python
 pycairo
 pydub
 pygments
-pyreadline; sys_platform == 'win32'
+pyreadline; sys_platform == 'win32',
+rich
diff --git a/setup.py b/setup.py
index 688d12981a..51b8279813 100755
--- a/setup.py
+++ b/setup.py
@@ -26,5 +26,6 @@
         "pydub",
         "pygments",
         "pyreadline; sys_platform == 'win32'",
+        "rich"
     ],
 )


Code review comments:
diff_hunk: @@ -0,0 +1,12 @@
+import logging
+from rich.logging import RichHandler
+
+## Setting config for logger.
path: manim/logger.py
text: @user1:
Not a very useful comment :) I would delete it

diff_hunk: @@ -133,7 +135,7 @@ def parse_cli():
         )
         return parser.parse_args()
     except argparse.ArgumentError as err:
-        print(str(err))
+        logger.error(str(err))
path: manim/config.py
text: @user1:
Do we want to also emit an extra warning message, like on line 234?