Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More graceful translations startup handling #630

Merged
merged 8 commits into from
Aug 25, 2024
112 changes: 61 additions & 51 deletions MCprep_addon/conf.py
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@

import bpy
from bpy.utils.previews import ImagePreviewCollection
import bpy.utils.previews


# -----------------------------------------------------------------------------
@@ -57,20 +58,13 @@ class Engine(enum.Enum):
Skin = Tuple[str, Path]
Entity = Tuple[str, str, str]

# Represents an unknown location
# for MCprepError. Given a global
# Represents an unknown location
# for MCprepError. Given a global
# constant to make it easier to use
# and check for
UNKNOWN_LOCATION = (-1, "UNKNOWN LOCATION")
DEBUG_MODE = False

# check if custom preview icons available
try:
import bpy.utils.previews
except:
print("MCprep: No custom icons in this blender instance")
pass


# -----------------------------------------------------------------------------
# ADDON GLOBAL VARIABLES AND INITIAL SETTINGS
@@ -81,11 +75,14 @@ class MCprepEnv:
def __init__(self):
self.data = None
self.json_data: Optional[Dict] = None
self.json_path: Path = Path(os.path.dirname(__file__), "MCprep_resources", "mcprep_data.json")
self.json_path_update: Path = Path(os.path.dirname(__file__), "MCprep_resources", "mcprep_data_update.json")
self.json_path: Path = Path(
os.path.dirname(__file__), "MCprep_resources", "mcprep_data.json")
self.json_path_update: Path = Path(
os.path.dirname(__file__), "MCprep_resources", "mcprep_data_update.json")

self.dev_file: Path = Path(os.path.dirname(__file__), "mcprep_dev.txt")
self.languages_folder: Path = Path(os.path.dirname(__file__), "MCprep_resources", "Languages")
self.languages_folder: Path = Path(
os.path.dirname(__file__), "MCprep_resources", "Languages")
self.translations: Path = Path(os.path.dirname(__file__), "translations.py")

self.last_check_for_updated = 0
@@ -137,30 +134,38 @@ def __init__(self):
# that no reading has occurred. If lib not found, will update to [].
# If ever changing the resource pack, should also reset to None.
self.material_sync_cache: List = []

# Whether we use PO files directly or use the converted form
self.use_direct_i18n = False
# i18n using Python's gettext module
#
# This only runs if translations.py does not exist
self.languages: dict[str, gettext.NullTranslations] = {}
self._load_translations()

def _load_translations(self, prefix="") -> None:
"""Loads in mo file translation maps"""
try:
if not self.translations.exists():
self.languages: dict[str, gettext.NullTranslations] = {}
print(prefix, "No translations.py, running in location: ", __file__, " with ", self.languages_folder)
for language in self.languages_folder.iterdir():
self.languages[language.name] = gettext.translation("mcprep",
self.languages_folder,
fallback=True,
languages=[language.name])
self.use_direct_i18n = True
self.log("Loaded direct i18n!")

except Exception:
print(prefix, "In langdir:", language.name)
self.languages[language.name] = gettext.translation(
"mcprep",
localedir=self.languages_folder,
fallback=True,
languages=[language.name])
print(prefix, "Loaded: ", language.name)
self.use_direct_i18n = True
self.log("Loaded direct i18n!")
except Exception as e:
self.languages = {}
self.log("Exception occured while loading translations!")
self.log(f"Exception occured while loading translations! {e}")
print(prefix, "Exception", e)
#raise e


# This allows us to translate strings on the fly
def _(self, msg: str) -> str:
"""Allows us to translate strings on the fly"""
if not self.use_direct_i18n:
return msg
if bpy.context.preferences.view.language in self.languages:
@@ -257,14 +262,14 @@ def current_line_and_file(self) -> Tuple[int, str]:
MCprepError.

This function can not return an MCprepError value as doing
so would be more complicated for the caller. As such, if
this fails, we return values -1 and "UNKNOWN LOCATION" to
indicate that we do not know the line number or file path
so would be more complicated for the caller. As such, if
this fails, we return values -1 and "UNKNOWN LOCATION" to
indicate that we do not know the line number or file path
the error occured on.

Returns:
- If success: Tuple[int, str] representing the current
line and file path
- If success: Tuple[int, str] representing the current
line and file path

- If fail: (-1, "UNKNOWN LOCATION")
"""
@@ -273,10 +278,10 @@ def current_line_and_file(self) -> Tuple[int, str]:
cur_frame = inspect.currentframe()
if not cur_frame:
return UNKNOWN_LOCATION

# Get the previous frame since the
# current frame is made for this function,
# not the function/code that called
# not the function/code that called
# this function
prev_frame = cur_frame.f_back
if not prev_frame:
@@ -285,50 +290,45 @@ def current_line_and_file(self) -> Tuple[int, str]:
frame_info = inspect.getframeinfo(prev_frame)
return frame_info.lineno, frame_info.filename


@dataclass
class MCprepError(object):
"""
Object that is returned when
Object that is returned when
an error occurs. This is meant
to give more information to the
caller so that a better error
to give more information to the
caller so that a better error
message can be made

Attributes
------------
err_type: BaseException
The error type; uses standard
The error type; uses standard
Python exceptions


line: int
Line the exception object was
created on. The preferred method
to do this is to use currentframe
and getframeinfo from the inspect
Line the exception object was
created on. The preferred method
to do this is to use currentframe
and getframeinfo from the inspect
module

file: str
Path of file the exception object
was created in. The preferred way
was created in. The preferred way
to get this is __file__

msg: Optional[str]
Optional message to display for an
exception. Use this if the exception
Optional message to display for an
exception. Use this if the exception
type may not be so clear cut
"""
err_type: BaseException
line: int
line: int
file: str
msg: Optional[str] = None

# Requires Extension support and building with the proper wheels
if DEBUG_MODE and bpy.app.version >= (4, 2, 0):
import debugpy
debugpy.listen(("localhost", 5678))

env = MCprepEnv()

def updater_select_link_function(self, tag):
"""Indicates what zip file to use for updating from a tag structure.
@@ -347,6 +347,16 @@ def updater_select_link_function(self, tag):
# GLOBAL REGISTRATOR INIT
# -----------------------------------------------------------------------------


# Requires Extension support and building with the proper wheels
if DEBUG_MODE and bpy.app.version >= (4, 2, 0):
import debugpy
debugpy.listen(("localhost", 5678))


env = MCprepEnv()


def register():
global env
if not env.json_data:
22 changes: 17 additions & 5 deletions run_tests.py
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@
import time


COMPILE_CMD = ["bab", "-b", "dev"]
COMPILE_CMD = ["bab", "-b", "dev", "translate"]
DATA_CMD = ["python", "mcprep_data_refresh.py", "-auto"] # TODO, include in build
DCC_EXES = "blender_execs.txt"
TEST_RUNNER = os.path.join("test_files", "test_runner.py")
@@ -60,6 +60,7 @@ def __str__(self):


def main():
t0 = time.time()
args = get_args()

# Read arguments
@@ -69,12 +70,21 @@ def main():
return

# Compile the addon
if args.version:
# Just install into the only blender version we'll test anyways
COMPILE_CMD.extend(["-v", args.version])
elif not args.all_execs:
# Just install into the first blender binary listed to match the tests
# TODO: get Blender version from the binary path at blender_execs[0]
# default_v = x
# COMPILE_CMD.extend(["-v", default_v])
pass
res = subprocess.check_output(COMPILE_CMD)
print("Compile output:", res.decode("utf-8"))
reset_test_file()

# Loop over all binaries and run tests.
t0 = time.time()
t1 = time.time()
any_failures = False
for ind, binary in enumerate(blender_execs):
run_all = args.all_execs is True
@@ -104,15 +114,17 @@ def main():
if child.returncode != 0:
any_failures = True

t1 = time.time()
t2 = time.time()

# Especially ensure tracker files are removed after tests complete.
remove_tracker_files()

output_results()
round_s = round(t1 - t0)
compile_time = t1 - t0
test_time = t2 - t1
exit_code = 1 if any_failures else 0
print(f"tests took {round_s}s to run, ending with code {exit_code}")
print(f"Compiled in {compile_time:.1f}s, tests ran in {test_time:.1f}s")
print(f"Finished in {compile_time + test_time:.1f}s with code {exit_code}")
sys.exit(exit_code)


66 changes: 66 additions & 0 deletions test_files/addon_test.py
Original file line number Diff line number Diff line change
@@ -16,14 +16,23 @@
#
# ##### END GPL LICENSE BLOCK #####

from pathlib import Path
import os
import unittest

import bpy

from MCprep_addon import conf


class AddonTest(unittest.TestCase):
"""Create addon level tests, and ensures enabled for later tests."""

@staticmethod
def addon_path() -> Path:
scripts = bpy.utils.user_resource("SCRIPTS")
return Path(scripts, "addons", "MCprep_addon")

def test_enable(self):
"""Ensure the addon can be directly enabled."""
bpy.ops.preferences.addon_enable(module="MCprep_addon")
@@ -33,6 +42,63 @@ def test_disable_enable(self):
bpy.ops.preferences.addon_disable(module="MCprep_addon")
bpy.ops.preferences.addon_enable(module="MCprep_addon")

def test_translations(self):
"""Safely ensures translations are working fine still.

Something go wrong, blender stuck in another language? Run in console:
bpy.context.preferences.view.language = "en_US"
"""
init_lang = bpy.context.preferences.view.language
try:
self._test_translation()
except Exception:
raise
finally:
bpy.context.preferences.view.language = init_lang

def _test_translation(self):
"""Ensure that creating the MCprep environment is error-free."""
test_env = conf.MCprepEnv()
mcprep_dir = self.addon_path()
lang_folder = mcprep_dir / "MCprep_resources" / "Languages"
test_env.languages_folder = lang_folder

print("Lang folder", lang_folder)
print("self.translations", test_env.use_direct_i18n)
print("Dir:", list(os.listdir(lang_folder)))

# Don't assign translations, to force building the map in memory
# translations_py = mcprep_dir / "translations.py"
# test_env.translations = translations_py

# Force load translations into this instance of MCprepEnv
test_env._load_translations(prefix=">>>\t")

# TODO: Not working, above load encounters error
self.assertIn(
"en_US", test_env.languages, "Missing default translation key")
self.assertTrue(
test_env.use_direct_i18n, "use_direct_i18n should be True")

# Magic string evaluations, will break if source po's change
test_translations = [
("ru_RU", "Restart blender", "Перезапустите блендер"),
("zh_HANS", "Texture pack folder", "材质包文件夹"),
("en_US", "Mob Spawner", "Mob Spawner"),
]
for lang, src, dst in test_translations:
with self.subTest(lang):
# First ensure the mo files exist
self.assertTrue(
os.path.isfile(
lang_folder / lang / "LC_MESSAGES" / "mcprep.mo"),
f"Missing {lang}'s mo file")
Comment on lines +88 to +91
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If nothing else, we could reduce the test down to just this, which already goes a long way to prove that translations are present when they should be.


# TODO: Not working
bpy.context.preferences.view.language = lang
res = test_env._(src)
self.assertEqual(res, dst, f"Unexpected {lang} translation)")


if __name__ == '__main__':
unittest.main(exit=False)