diff --git a/manim/default.cfg b/manim/default.cfg index a5006bd2f7..54c9607949 100644 --- a/manim/default.cfg +++ b/manim/default.cfg @@ -170,6 +170,7 @@ log_message = log_path = dim log_width = -1 log_height = -1 +log_timestamps = True [ffmpeg] # Uncomment the following line to manually set the loglevel for ffmpeg. See ffmpeg manpage for accepted values diff --git a/manim/logger.py b/manim/logger.py index a8f83ca834..c83745b4a8 100644 --- a/manim/logger.py +++ b/manim/logger.py @@ -30,9 +30,14 @@ def parse_theme(fp): theme["log.height"] = ( None if theme["log.height"] == "-1" else int(theme["log.height"]) ) + theme["log.timestamps"] = config_parser["logger"].getboolean("log.timestamps") try: customTheme = Theme( - {k: v for k, v in theme.items() if k not in ["log.width", "log.height"]} + { + k: v + for k, v in theme.items() + if k not in ["log.width", "log.height", "log.timestamps"] + } ) except (color.ColorParseError, errors.StyleSyntaxError): customTheme = None @@ -80,7 +85,7 @@ def parse_theme(fp): level="NOTSET", format="%(message)s", datefmt="[%X]", - handlers=[RichHandler(console=console)], + handlers=[RichHandler(console=console, show_time=themedict["log.timestamps"])], ) logger = logging.getLogger("rich") diff --git a/manim/utils/cfg_subcmds.py b/manim/utils/cfg_subcmds.py index f48b4abcdc..4f23e5dae9 100644 --- a/manim/utils/cfg_subcmds.py +++ b/manim/utils/cfg_subcmds.py @@ -8,12 +8,12 @@ """ import os import configparser +from ast import literal_eval from .config_utils import _run_config, _paths_config_file, finalized_configs_dict from .file_ops import guarantee_existence, open_file from rich.console import Console -from rich.progress import track from rich.style import Style from rich.errors import StyleSyntaxError @@ -22,10 +22,54 @@ RICH_COLOUR_INSTRUCTIONS = """[red]The default colour is used by the input statement. If left empty, the default colour will be used.[/red] [magenta] For a full list of styles, visit[/magenta] [green]https://rich.readthedocs.io/en/latest/style.html[/green]""" - +RICH_NON_STYLE_ENTRIES = ["log.width", "log.height", "log.timestamps"] console = Console() +def value_from_string(value): + """Extracts the literal of proper datatype from a string. + Parameters + ---------- + value : :class:`str` + The value to check get the literal from. + + Returns + ------- + Union[:class:`str`, :class:`int`, :class:`bool`] + Returns the literal of appropriate datatype. + """ + try: + value = literal_eval(value) + except (SyntaxError, ValueError): + pass + return value + + +def _is_expected_datatype(value, expected, style=False): + """Checks whether `value` is the same datatype as `expected`, + and checks if it is a valid `style` if `style` is true. + + Parameters + ---------- + value : :class:`str` + The string of the value to check (obtained from reading the user input). + expected : :class:`str` + The string of the literal datatype must be matched by `value`. Obtained from + reading the cfg file. + style : :class:`bool`, optional + Whether or not to confirm if `value` is a style, by default False + + Returns + ------- + :class:`bool` + Whether or not `value` matches the datatype of `expected`. + """ + value = value_from_string(value) + expected = type(value_from_string(expected)) + + return isinstance(value, expected) and (is_valid_style(value) if style else True) + + def is_valid_style(style): """Checks whether the entered color is a valid color according to rich Parameters @@ -81,62 +125,61 @@ def write(level=None, openfile=False): you will have to create a manim.cfg in the local directory, where you want those changes to be overridden.""" CWD_CONFIG_MSG = f"""A configuration file at [yellow]{config_paths[2]}[/yellow] has been created. -To save your theme please save that file and place it in your current working directory, from where you run the manim command.""" +To save your config please save that file and place it in your current working directory, from where you run the manim command.""" if not openfile: action = "save this as" - for category in config: console.print(f"{category}", style="bold green underline") default = config[category] if category == "logger": console.print(RICH_COLOUR_INSTRUCTIONS) default = replace_keys(default) - for key in default: - desc = ( - "style" if key not in ["log.width", "log.height"] else "value" - ) - style = key if key not in ["log.width", "log.height"] else None - cond = ( - is_valid_style - if key not in ["log.width", "log.height"] - else lambda m: m.isdigit() + + for key in default: + # All the cfg entries for logger need to be validated as styles, + # as long as they arent setting the log width or height etc + if category == "logger" and key not in RICH_NON_STYLE_ENTRIES: + desc = "style" + style = default[key] + else: + desc = "value" + style = None + + console.print(f"Enter the {desc} for {key} ", style=style, end="") + if category != "logger" or key in RICH_NON_STYLE_ENTRIES: + defaultval = ( + repr(default[key]) + if isinstance(value_from_string(default[key]), str) + else default[key] ) - console.print(f"Enter the {desc} for {key}:", style=style, end="") + console.print(f"(defaults to {defaultval}) :", end="") + try: temp = input() - if temp: - while not cond(temp): - console.print( - f"[red bold]Invalid {desc}. Try again.[/red bold]" - ) - console.print( - f"Enter the {desc} for {key}:", style=style, end="" - ) - temp = input() - else: - default[key] = temp - default = replace_keys(default) - - else: - for key in default: - if default[key] in ["True", "False"]: + except EOFError: + raise Exception( + """Not enough values in input. +You may have added a new entry to default.cfg, in which case you will have to +modify write_cfg_subcmd_input to account for it.""" + ) + if temp: + while temp and not _is_expected_datatype( + temp, default[key], bool(style) + ): + console.print( + f"[red bold]Invalid {desc}. Try again.[/red bold]" + ) console.print( - f"Enter value for {key} (defaults to {default[key]}):", - end="", + f"Enter the {desc} for {key}:", style=style, end="" ) temp = input() - if temp: - while not temp.lower().capitalize() in ["True", "False"]: - console.print( - "[red bold]Invalid value. Try again.[/red bold]" - ) - console.print( - f"Enter the style for {key}:", style=key, end="" - ) - temp = input() - else: - default[key] = temp + else: + default[key] = temp + + default = replace_keys(default) if category == "logger" else default + config[category] = dict(default) + else: action = "open" @@ -168,10 +211,11 @@ def write(level=None, openfile=False): def show(): current_config = finalized_configs_dict() + rich_non_style_entries = [a.replace(".", "_") for a in RICH_NON_STYLE_ENTRIES] for category in current_config: console.print(f"{category}", style="bold green underline") for entry in current_config[category]: - if category == "logger" and entry not in ["log_width", "log_height"]: + if category == "logger" and entry not in rich_non_style_entries: console.print(f"{entry} :", end="") console.print( f" {current_config[category][entry]}", diff --git a/manim/utils/config_utils.py b/manim/utils/config_utils.py index 7180e02901..7d8c175968 100644 --- a/manim/utils/config_utils.py +++ b/manim/utils/config_utils.py @@ -23,8 +23,6 @@ "finalized_configs_dict", ] -min_argvs = 3 if "-m" in sys.argv[0] else 2 - def _parse_file_writer_config(config_parser, args): """Parse config files and CLI arguments into a single dictionary.""" @@ -169,29 +167,19 @@ def _parse_cli(arg_list, input=True): if input: # If the only command is `manim`, we want both subcommands like `cfg` # and mandatory positional arguments like `file` to show up in the help section. - if len(sys.argv) == min_argvs - 1 or _subcommands_exist(): + only_manim = len(sys.argv) == 1 + + if only_manim or _subcommand_name(): subparsers = parser.add_subparsers(dest="subcommands") - cfg_related = subparsers.add_parser("cfg") - cfg_subparsers = cfg_related.add_subparsers(dest="cfg_subcommand") - - cfg_write_parser = cfg_subparsers.add_parser("write") - cfg_write_parser.add_argument( - "--level", - choices=["user", "cwd"], - default=None, - help="Specify if this config is for user or just the working directory.", - ) - cfg_write_parser.add_argument( - "--open", action="store_const", const=True, default=False - ) - cfg_subparsers.add_parser("show") - cfg_export_parser = cfg_subparsers.add_parser("export") - cfg_export_parser.add_argument("--dir", default=os.getcwd()) + # More subcommands can be added here, with elif statements. + # If a help command is passed, we still want subcommands to show + # up, so we check for help commands as well before adding the + # subcommand's subparser. + if only_manim or _subcommand_name() in ["cfg", "--help", "-h"]: + cfg_related = _init_cfg_subcmd(subparsers) - if len(sys.argv) == min_argvs - 1 or not _subcommands_exist( - ignore=["--help", "-h"] - ): + if only_manim or not _subcommand_name(ignore=["--help", "-h"]): parser.add_argument( "file", help="path to file holding the python code for the scene", ) @@ -420,13 +408,12 @@ def _str2bool(s): ) parsed = parser.parse_args(arg_list) if hasattr(parsed, "subcommands"): - setattr( - parsed, - "cfg_subcommand", - cfg_related.parse_args( - sys.argv[min_argvs - (0 if min_argvs == 2 else 1) :] - ).cfg_subcommand, - ) + if _subcommand_name() == "cfg": + setattr( + parsed, + "cfg_subcommand", + cfg_related.parse_args(sys.argv[2:]).cfg_subcommand, + ) return parsed @@ -537,10 +524,72 @@ def finalized_configs_dict(): return {section: dict(config[section]) for section in config.sections()} -def _subcommands_exist(ignore=[]): +def _subcommand_name(ignore=()): + """Goes through sys.argv to check if any subcommand has been passed, + and returns the first such subcommand's name, if found. + + Parameters + ---------- + ignore : Iterable[:class:`str`], optional + List of NON_ANIM_UTILS to ignore when searching for subcommands, by default [] + + Returns + ------- + Optional[:class:`str`] + If a subcommand is found, returns the string of its name. Returns None if no + subcommand is found. + """ NON_ANIM_UTILS = ["cfg", "--help", "-h"] NON_ANIM_UTILS = [util for util in NON_ANIM_UTILS if util not in ignore] - not_only_manim = len(sys.argv) > min_argvs - 1 - sub_command_exists = any(a == item for a in sys.argv for item in NON_ANIM_UTILS) - return not_only_manim and sub_command_exists + # If a subcommand is found, break out of the inner loop, and hit the break of the outer loop + # on the way out, effectively breaking out of both loops. The value of arg will be the + # subcommand to be taken. + # If no subcommand is found, none of the breaks are hit, and the else clause of the outer loop + # is run, setting arg to None. + + for item in NON_ANIM_UTILS: + for arg in sys.argv: + if arg == item: + break + else: + continue + break + else: + arg = None + + return arg + + +def _init_cfg_subcmd(subparsers): + """Initialises the subparser for the `cfg` subcommand. + + Parameters + ---------- + subparsers : :class:`argparse._SubParsersAction` + The subparser object for which to add the sub-subparser for the cfg subcommand. + + Returns + ------- + :class:`argparse.ArgumentParser` + The parser that parser anything cfg subcommand related. + """ + cfg_related = subparsers.add_parser("cfg",) + cfg_subparsers = cfg_related.add_subparsers(dest="cfg_subcommand") + + cfg_write_parser = cfg_subparsers.add_parser("write") + cfg_write_parser.add_argument( + "--level", + choices=["user", "cwd"], + default=None, + help="Specify if this config is for user or just the working directory.", + ) + cfg_write_parser.add_argument( + "--open", action="store_const", const=True, default=False + ) + cfg_subparsers.add_parser("show") + + cfg_export_parser = cfg_subparsers.add_parser("export") + cfg_export_parser.add_argument("--dir", default=os.getcwd()) + + return cfg_related diff --git a/tests/test_cli/write_cfg_sbcmd_input.txt b/tests/test_cli/write_cfg_sbcmd_input.txt index ee5bbcfcae..1f449281a9 100644 --- a/tests/test_cli/write_cfg_sbcmd_input.txt +++ b/tests/test_cli/write_cfg_sbcmd_input.txt @@ -6,6 +6,7 @@ + False @@ -27,5 +28,42 @@ False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_logging/expected.txt b/tests/test_logging/expected.txt index 9708fae5ed..fc4ad658a1 100644 --- a/tests/test_logging/expected.txt +++ b/tests/test_logging/expected.txt @@ -1,4 +1,4 @@ - INFO Read configuration files: config.py: - INFO scene_file_writer.py: - File ready at +INFO Read configuration files: config.py: +INFO scene_file_writer.py: + File ready at diff --git a/tests/test_logging/test_logging.py b/tests/test_logging/test_logging.py index 05b9dbbefa..4fc7ba92f9 100644 --- a/tests/test_logging/test_logging.py +++ b/tests/test_logging/test_logging.py @@ -45,8 +45,8 @@ def test_logging_to_file(python_version): enc = "utf-8" with open(log_file_path, encoding=enc) as logfile: logs = logfile.read() - # The following regex pattern selects timestamps, file paths and all numbers.. - pattern = r"(\[?\d+:?]?)|(\['[A-Z]?:?[\/\\].*cfg'])|([A-Z]?:?[\/\\].*mp4)" + # The following regex pattern selects file paths and all numbers. + pattern = r"(\['[A-Z]?:?[\/\\].*cfg'])|([A-Z]?:?[\/\\].*mp4)|(\d+)" logs = re.sub(pattern, lambda m: " " * len((m.group(0))), logs) with open( diff --git a/tests/tests_data/manim.cfg b/tests/tests_data/manim.cfg index 9986617c4c..a1b7f8a381 100644 --- a/tests/tests_data/manim.cfg +++ b/tests/tests_data/manim.cfg @@ -6,4 +6,5 @@ save_last_frame = False # save_pngs = False [logger] -log_width = 512 \ No newline at end of file +log_width = 512 +log_timestamps = False