From 5ea1d87974f4edf557e09b2f143766c32474af16 Mon Sep 17 00:00:00 2001 From: Faraphel Date: Sun, 24 Jul 2022 21:46:41 +0200 Subject: [PATCH] implemented macros to make safe_eval expression more readable. --- source/mkw/ModConfig.py | 16 +++++++++++----- source/mkw/Patch/Patch.py | 1 + source/safe_eval.py | 33 +++++++++++++++++++++++++++++---- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/source/mkw/ModConfig.py b/source/mkw/ModConfig.py index 9d73502..f4a48bc 100644 --- a/source/mkw/ModConfig.py +++ b/source/mkw/ModConfig.py @@ -27,7 +27,7 @@ class ModConfig: __slots__ = ("name", "path", "nickname", "variant", "tags_prefix", "tags_suffix", "default_track", "_tracks", "version", "original_track_prefix", "swap_original_order", "keep_original_track", "enable_random_cup", "tags_cups", "track_file_template", - "multiplayer_disable_if") + "multiplayer_disable_if", "macros") def __init__(self, path: Path | str, name: str, nickname: str = None, version: str = None, variant: str = None, tags_prefix: dict[Tag, str] = None, tags_suffix: dict[Tag, str] = None, @@ -35,9 +35,10 @@ class ModConfig: default_track: "Track | TrackGroup" = None, tracks: list["Track | TrackGroup"] = None, original_track_prefix: bool = None, swap_original_order: bool = None, keep_original_track: bool = None, enable_random_cup: bool = None, - track_file_template: str = None, multiplayer_disable_if: str = None): + track_file_template: str = None, multiplayer_disable_if: str = None, macros: dict | None = None): self.path = Path(path) + self.macros: dict = macros if macros is not None else {} self.name: str = name self.nickname: str = nickname if nickname is not None else name @@ -63,17 +64,18 @@ class ModConfig: return f"" @classmethod - def from_dict(cls, path: Path | str, config_dict: dict) -> "ModConfig": + def from_dict(cls, path: Path | str, config_dict: dict, macros: dict | None) -> "ModConfig": """ Create a ModConfig from a dict :param path: path of the mod_config.json :param config_dict: dict containing the configuration + :param macros: macro that can be used for safe_eval :return: ModConfig """ kwargs = { attr: config_dict.get(attr) for attr in cls.__slots__ - if attr not in ["name", "default_track", "_tracks", "tracks", "path"] + if attr not in ["name", "default_track", "_tracks", "tracks", "path", "macros"] # these keys are treated after or are reserved } @@ -85,6 +87,7 @@ class ModConfig: default_track=Track.from_dict(config_dict.get("default_track", {})), tracks=[Track.from_dict(track) for track in config_dict.get("tracks", [])], + macros=macros, ) @classmethod @@ -95,9 +98,12 @@ class ModConfig: :return: ModConfig """ config_file = Path(config_file) + macros_file = config_file.parent / "macros.json" + return cls.from_dict( path=config_file, - config_dict=json.loads(config_file.read_text(encoding="utf8")) + config_dict=json.loads(config_file.read_text(encoding="utf8")), + macros=json.loads(macros_file.read_text(encoding="utf8")) if macros_file.exists() else None, ) def get_mod_directory(self) -> Path: diff --git a/source/mkw/Patch/Patch.py b/source/mkw/Patch/Patch.py index 6117b99..1e91847 100644 --- a/source/mkw/Patch/Patch.py +++ b/source/mkw/Patch/Patch.py @@ -28,6 +28,7 @@ class Patch: return (multiple_safe_eval if multiple else safe_eval)( template, env={"mod_config": self.mod_config} | (env if env is not None else {}), + macros=self.mod_config.macros, ) def install(self, extracted_game: "ExtractedGame") -> Generator[dict, None, None]: diff --git a/source/safe_eval.py b/source/safe_eval.py index bf41585..c927b2f 100644 --- a/source/safe_eval.py +++ b/source/safe_eval.py @@ -17,11 +17,17 @@ common_token_map = { # these operators and function are considered safe to use } TOKEN_START, TOKEN_END = "{{", "}}" +MACRO_START, MACRO_END = "##", "##" class TemplateParsingError(Exception): def __init__(self, token: str): - super().__init__(f"Invalid token while parsing track representation:\n{token}") + super().__init__(f"Invalid token while parsing safe_eval:\n{token}") + + +class NotImplementedMacro(Exception): + def __init__(self, macro: str): + super().__init__(f"Invalid macro while parsing macros:\n{macro}") class SafeFunction: @@ -51,14 +57,33 @@ class SafeFunction: return attr -def safe_eval(template: str, env: dict[str, any] = None) -> str: +def replace_macro(template: str, macros: dict[str, str]) -> str: + """ + Replace all the macro defined in macro by their respective value + :param template: template where to replace the macro + :param macros: dictionary associating macro with their replacement + :return: the template with macro replaced + """ + + def format_macro(match: re.Match) -> str: + if (macro := macros.get(match.group(1).strip())) is None: raise NotImplementedMacro(macro) + return macro + + # match everything between MACRO_START and MACRO_END. + return re.sub(rf"{MACRO_START}(.*?){MACRO_END}", format_macro, template) + + +def safe_eval(template: str, env: dict[str, any] = None, macros: dict[str, str] = None) -> str: """ Evaluate the template and return the result in a safe way :param env: variables to use when using eval :param template: template to evaluate + :param macros: additionnal macro to replace in the template """ if env is None: env = {} + if macros is None: macros = {} + template = replace_macro(template, macros) token_map: dict[str, str] = common_token_map | {var: var for var in env} final_token: str = "" @@ -111,7 +136,7 @@ def safe_eval(template: str, env: dict[str, any] = None) -> str: else: return final_token -def multiple_safe_eval(template: str, env: dict[str, any] = None) -> str: +def multiple_safe_eval(template: str, env: dict[str, any] = None, macros: dict[str, str] = None) -> str: def format_part_template(match: re.Match) -> str: """ when a token is found, replace it by the corresponding value @@ -120,7 +145,7 @@ def multiple_safe_eval(template: str, env: dict[str, any] = None) -> str: """ # get the token string without the brackets, then strip it. Also double antislash part_template = match.group(1).strip().replace("\\", "\\\\") - return safe_eval(part_template, env) + return safe_eval(part_template, env, macros) # pass everything between TOKEN_START and TOKEN_END in the function return re.sub(rf"{TOKEN_START}(.*?){TOKEN_END}", format_part_template, template)