implemented macros to make safe_eval expression more readable.

This commit is contained in:
Faraphel 2022-07-24 21:46:41 +02:00
parent 62a2e31ce2
commit 5ea1d87974
3 changed files with 41 additions and 9 deletions

View file

@ -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"<ModConfig name={self.name} version={self.version}>"
@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:

View file

@ -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]:

View file

@ -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)