From 6c4aedb17af540de9e96967c21cbdc5781da1e11 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Tue, 8 Jan 2019 20:55:20 -0500 Subject: [PATCH] Replace fancy AgeSDL Python meta hacks Path of the Shell did not like my fancy metaprogramming tricks for defining an AgeSDL Python class that contained characters that are illegal in Python identifiers. So, now, we revert to just using a standard class declaration. That means that we need to strip out any illegal identifiers from the age name first. A legal Python 2.x identifier is constrained to the ASCII alphanumeric characters and the underscore with the stipulation that the first character cannot be a number. To illustrate this to the artist, we alert the age name property field if an illegal character is found in the age name. We also alert on the underscore, which is now used as a very very special replacement character. In the case of an illegal character, an error message is shown in the UI with the correct AgeSDL name. Of course, I hope no one really uses those illegal characters and this is just more fulmination on my part... --- korman/exporter/manager.py | 10 ++++++---- korman/exporter/python.py | 7 ++++--- korman/korlib/__init__.py | 35 +++++++++++++++++++++++++++++++++++ korman/operators/op_export.py | 12 +++++++++++- korman/plasma_magic.py | 3 ++- korman/ui/ui_world.py | 17 ++++++++++++++--- 6 files changed, 72 insertions(+), 12 deletions(-) diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py index c3906c7..380caa6 100644 --- a/korman/exporter/manager.py +++ b/korman/exporter/manager.py @@ -19,6 +19,7 @@ from PyHSPlasma import * import weakref from . import explosions +from .. import korlib from ..plasma_magic import * # These objects have to be in the plSceneNode pool in order to be loaded... @@ -243,21 +244,22 @@ class ExportManager: output = self._exporter().output # AgeSDL Hook Python - py_filename = "{}.py".format(age) + fixed_agename = korlib.replace_python2_identifier(age) + py_filename = "{}.py".format(fixed_agename) age_py = get_text(py_filename, None) if output.want_py_text(age_py): py_code = age_py.as_string() else: - py_code = very_very_special_python.format(age_name=age).lstrip() + py_code = very_very_special_python.format(age_name=fixed_agename).lstrip() output.add_python_mod(py_filename, text_id=age_py, str_data=py_code) # AgeSDL - sdl_filename = "{}.sdl".format(age) + sdl_filename = "{}.sdl".format(fixed_agename) age_sdl = get_text(sdl_filename) if age_sdl is not None: sdl_code = None else: - sdl_code = very_very_special_sdl.format(age_name=age).lstrip() + sdl_code = very_very_special_sdl.format(age_name=fixed_agename).lstrip() output.add_sdl(sdl_filename, text_id=age_sdl, str_data=sdl_code) def save_age(self): diff --git a/korman/exporter/python.py b/korman/exporter/python.py index e263b9f..ef87718 100644 --- a/korman/exporter/python.py +++ b/korman/exporter/python.py @@ -80,7 +80,8 @@ class PythonPackageExporter: def _ensure_age_sdl_hook(self, report): age_props = bpy.context.scene.world.plasma_age if age_props.age_sdl: - py_filename = "{}.py".format(age_props.age_name) + fixed_agename = korlib.replace_python2_identifier(age_props.age_name) + py_filename = "{}.py".format(fixed_agename) age_py = self._modules.get(py_filename) if age_py is not None: del self._modules[py_filename] @@ -88,11 +89,11 @@ class PythonPackageExporter: self._pfms[py_filename] = age_py else: report.warn("AgeSDL Python Script provided, but not requested for packing... Using default Python.", indent=1) - self._pfms[py_filename] = very_very_special_python.format(age_name=age_props.age_name) + self._pfms[py_filename] = very_very_special_python.format(age_name=fixed_agename) else: report.msg("Packing default AgeSDL Python", indent=1) very_very_special_python.format(age_name=age_props.age_name) - self._pfms[py_filename] = very_very_special_python.format(age_name=age_props.age_name) + self._pfms[py_filename] = very_very_special_python.format(age_name=fixed_agename) def _harvest_pfms(self, report): objects = bpy.context.scene.objects diff --git a/korman/korlib/__init__.py b/korman/korlib/__init__.py index 3e03574..78a0fe9 100644 --- a/korman/korlib/__init__.py +++ b/korman/korlib/__init__.py @@ -76,6 +76,13 @@ finally: from .python import * from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY + _IDENTIFIER_RANGES = ((ord('0'), ord('9')), (ord('A'), ord('Z')), (ord('a'), ord('z'))) + from keyword import kwlist as _kwlist + _KEYWORDS = set(_kwlist) + # Python 2.x keywords + _KEYWORDS.add("exec") + _KEYWORDS.add("print") + def _wave_chunks(stream): while not stream.eof(): chunk_name = stream.read(4) @@ -101,3 +108,31 @@ finally: header.read(stream) return chunks[b"data"]["size"] + + def is_legal_python2_identifier(identifier): + if not identifier: + return False + + # FIXME: str.isascii in Python 3.7 + if any(ord(i) > 0x7F for i in identifier): + return False + if is_python_keyword(identifier): + return False + return identifier.isidentifier() + + is_python_keyword = _KEYWORDS.__contains__ + + def replace_python2_identifier(identifier): + """Replaces illegal characters in a Python identifier with a replacement character""" + + def process(identifier): + # No leading digits in identifiers, so skip the first range element (0...9) + yield next((identifier[0] for low, high in _IDENTIFIER_RANGES[1:] + if low <= ord(identifier[0]) <= high), '_') + for i in identifier[1:]: + yield next((i for low, high in _IDENTIFIER_RANGES if low <= ord(i) <= high), '_') + + if identifier: + return "".join(process(identifier)) + else: + return "" diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py index fa8040b..b83ea5c 100644 --- a/korman/operators/op_export.py +++ b/korman/operators/op_export.py @@ -163,8 +163,12 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator): if context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") - # Separate blender operator and actual export logic for my sanity ageName = path.stem + if korlib.is_python_keyword(ageName): + self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(ageName)) + return {"CANCELLED"} + + # Separate blender operator and actual export logic for my sanity with UiHelper(context) as _ui: e = exporter.Exporter(self) try: @@ -256,6 +260,12 @@ class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator): return {"CANCELLED"} path.touch() + # Age names cannot be python keywords + age_name = context.scene.world.plasma_age.age_name + if korlib.is_python_keyword(age_name): + self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(age_name)) + return {"CANCELLED"} + # Bonus Fun: Implement Profile-mode here (later...) e = exporter.PythonPackageExporter(filepath=self.filepath, version=globals()[self.version]) diff --git a/korman/plasma_magic.py b/korman/plasma_magic.py index b67d438..ed89987 100644 --- a/korman/plasma_magic.py +++ b/korman/plasma_magic.py @@ -17,7 +17,8 @@ very_very_special_python = """ from Plasma import * from PlasmaTypes import * -globals()["{age_name}"] = type("{age_name}", (ptResponder,), dict()) +class {age_name}(ptResponder): + pass """ very_very_special_sdl = """ diff --git a/korman/ui/ui_world.py b/korman/ui/ui_world.py index 938f0f3..6618bfb 100644 --- a/korman/ui/ui_world.py +++ b/korman/ui/ui_world.py @@ -16,7 +16,7 @@ import bpy from pathlib import Path -from ..korlib import ConsoleToggler +from .. import korlib class AgeButtonsPanel: @@ -128,6 +128,9 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel): col.prop(active_page, "seq_suffix") col.prop_menu_enum(active_page, "version") + # Age Names should really be legal Python 2.x identifiers for AgeSDLHooks + legal_identifier = korlib.is_legal_python2_identifier(age.age_name) + # Core settings layout.separator() split = layout.split() @@ -140,15 +143,23 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel): col = split.column() col.label("Age Settings:") col.prop(age, "seq_prefix", text="ID") - col.alert = not age.age_name.strip() + col.alert = not legal_identifier or '_' in age.age_name col.prop(age, "age_name", text="") + # Display a hint if the identifier is illegal + if not legal_identifier: + if korlib.is_python_keyword(age.age_name): + layout.label(text="Ages should not be named the same as a Python keyword", icon="ERROR") + elif age.age_sdl: + fixed_identifier = korlib.replace_python2_identifier(age.age_name) + layout.label(text="Age's SDL will use the name '{}'".format(fixed_identifier), icon="ERROR") + layout.separator() split = layout.split() col = split.column() col.label("Export Settings:") - col.enabled = ConsoleToggler.is_platform_supported() + col.enabled = korlib.ConsoleToggler.is_platform_supported() col.prop(age, "verbose") col.prop(age, "show_console")