Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'tests/python/bl_keymap_validate.py')
-rw-r--r--tests/python/bl_keymap_validate.py212
1 files changed, 165 insertions, 47 deletions
diff --git a/tests/python/bl_keymap_validate.py b/tests/python/bl_keymap_validate.py
index ce2022f6ae7..1743893dc8a 100644
--- a/tests/python/bl_keymap_validate.py
+++ b/tests/python/bl_keymap_validate.py
@@ -16,6 +16,7 @@ This catches the following kinds of issues:
- Unused keymaps (keymaps which are defined but not used anywhere).
- Event values that don't make sense for the event type, e.g.
An escape key could have the value "NORTH" instead of "PRESS".
+- Identical key-map items.
This works by taking the keymap data (before it's loaded into Blender),
then comparing it with that same keymap after exporting and importing.
@@ -27,16 +28,46 @@ NOTE:
"""
import types
-import typing
-
+from typing import (
+ Any,
+ Dict,
+ Generator,
+ List,
+ Optional,
+ Sequence,
+ Tuple,
+)
+
+KeyConfigData = List[Tuple[str, Tuple[Any], Dict[str, Any]]]
+
+import os
import contextlib
-import bpy
+import bpy # type: ignore
# Useful for diffing the output to see what changed in context.
# this writes keymaps into the current directory with `.orig.py` & `.rewrite.py` extensions.
-WRITE_OUTPUT_DIR = None # "/tmp", defaults to the systems temp directory.
+WRITE_OUTPUT_DIR = "" # "/tmp", defaults to the systems temp directory.
+# For each preset, test all of these options.
+# The key is the preset name, containing a sequence of (attribute, value) pairs to test.
+#
+# NOTE(@campbellbarton): only add these for preferences which impact multiple keys as exposing all preferences
+# this way would create too many combinations making the tests take too long to complete.
+PRESET_PREFS = {
+ "Blender": (
+ (("select_mouse", 'LEFT'), ("use_alt_tool", False)),
+ (("select_mouse", 'LEFT'), ("use_alt_tool", True)),
+ (("select_mouse", 'RIGHT'), ("rmb_action", 'TWEAK')),
+ (("select_mouse", 'RIGHT'), ("rmb_action", 'FALLBACK_TOOL')),
+ ),
+}
+
+# Don't report duplicates for these presets.
+ALLOW_DUPLICATES = {
+ # This key-map manipulates the default key-map, making it difficult to avoid duplicates entirely.
+ "Industry_Compatible"
+}
# -----------------------------------------------------------------------------
# Generic Utilities
@@ -45,7 +76,7 @@ WRITE_OUTPUT_DIR = None # "/tmp", defaults to the systems temp directory.
def temp_fn_argument_extractor(
mod: types.ModuleType,
mod_attr: str,
-) -> typing.Iterator[typing.List[typing.Tuple[list, dict]]]:
+) -> Generator[List[Tuple[Tuple[Tuple[Any], ...], Dict[str, Dict[str, Any]]]], None, None]:
"""
Temporarily intercept a function, so it's arguments can be extracted.
The context manager gives us a list where each item is a tuple of
@@ -54,7 +85,7 @@ def temp_fn_argument_extractor(
args_collected = []
real_fn = getattr(mod, mod_attr)
- def wrap_fn(*args, **kw):
+ def wrap_fn(*args: Tuple[Any], **kw: Dict[str, Any]) -> Any:
args_collected.append((args, kw))
return real_fn(*args, **kw)
setattr(mod, mod_attr, wrap_fn)
@@ -66,10 +97,10 @@ def temp_fn_argument_extractor(
def round_float_32(f: float) -> float:
from struct import pack, unpack
- return unpack("f", pack("f", f))[0]
+ return unpack("f", pack("f", f))[0] # type: ignore
-def report_humanly_readable_difference(a: typing.Any, b: typing.Any) -> typing.Optional[str]:
+def report_humanly_readable_difference(a: Any, b: Any) -> Optional[str]:
"""
Compare strings, return None whrn they match,
otherwise a humanly readable difference message.
@@ -86,7 +117,7 @@ def report_humanly_readable_difference(a: typing.Any, b: typing.Any) -> typing.O
# -----------------------------------------------------------------------------
# Keymap Utilities.
-def keyconfig_preset_scan() -> typing.List[str]:
+def keyconfig_preset_scan() -> List[str]:
"""
Return all bundled presets (keymaps), not user presets.
"""
@@ -104,7 +135,7 @@ def keyconfig_preset_scan() -> typing.List[str]:
]
-def keymap_item_property_clean(value: typing.Any) -> typing.Any:
+def keymap_item_property_clean(value: Any) -> Any:
"""
Recursive property sanitize.
@@ -118,12 +149,13 @@ def keymap_item_property_clean(value: typing.Any) -> typing.Any:
return sorted(
# Convert to `dict` to de-duplicate.
dict([(k, keymap_item_property_clean(v)) for k, v in value]).items(),
- key=lambda item: item[0],
+ # Ignore type checking, these are strings which we know can be sorted.
+ key=lambda item: item[0], # type: ignore
)
return value
-def keymap_data_clean(keyconfig_data: typing.List, *, relaxed: bool) -> None:
+def keymap_data_clean(keyconfig_data: KeyConfigData, *, relaxed: bool) -> None:
"""
Order & sanitize keymap data so the result
from the hand written Python script is comparable with data exported & imported.
@@ -153,22 +185,82 @@ def keymap_data_clean(keyconfig_data: typing.List, *, relaxed: bool) -> None:
items[i] = item_op, item_event, None
-def keyconfig_activate_and_extract_data(filepath: str, *, relaxed: bool) -> typing.List:
+def keyconfig_config_as_filename_component(values: Sequence[Tuple[str, Any]]) -> str:
+ """
+ Takes a configuration, eg:
+
+ [("select_mouse", 'LEFT'), ("rmb_action", 'TWEAK')]
+
+ And returns a filename compatible path:
+ """
+ from urllib.parse import quote
+ if not values:
+ return ""
+
+ return "(" + quote(
+ ".".join([
+ "-".join((str(key), str(val)))
+ for key, val in values
+ ]),
+ # Needed so forward slashes aren't included in the resulting name.
+ safe="",
+ ) + ")"
+
+
+def keyconfig_activate_and_extract_data(
+ filepath: str,
+ *,
+ relaxed: bool,
+ config: Sequence[Tuple[str, Any]],
+) -> KeyConfigData:
"""
Activate the key-map by filepath,
return the key-config data (cleaned for comparison).
"""
- import bl_keymap_utils.io
+ import bl_keymap_utils.io # type: ignore
+
+ if config:
+ bpy.ops.preferences.keyconfig_activate(filepath=filepath)
+ km_prefs = bpy.context.window_manager.keyconfigs.active.preferences
+ for attr, value in config:
+ setattr(km_prefs, attr, value)
+
with temp_fn_argument_extractor(bl_keymap_utils.io, "keyconfig_init_from_data") as args_collected:
bpy.ops.preferences.keyconfig_activate(filepath=filepath)
+
# If called multiple times, something strange is happening.
assert(len(args_collected) == 1)
args, _kw = args_collected[0]
- keyconfig_data = args[1]
+ # Ignore the type check as `temp_fn_argument_extractor` is a generic function
+ # which doesn't contain type information of the function being wrapped.
+ keyconfig_data: KeyConfigData = args[1] # type: ignore
keymap_data_clean(keyconfig_data, relaxed=relaxed)
return keyconfig_data
+def keyconfig_report_duplicates(keyconfig_data: KeyConfigData) -> str:
+ """
+ Return true if any of the key-maps have duplicate items.
+
+ Duplicate items are reported so they can be resolved.
+ """
+ error_text = []
+ for km_idname, km_args, km_items_data in keyconfig_data:
+ items = tuple(km_items_data["items"])
+ unique: Dict[str, List[int]] = {}
+ for i, (item_op, item_event, item_prop) in enumerate(items):
+ # Ensure stable order as `repr` will use order of definition.
+ item_event = {key: item_event[key] for key in sorted(item_event.keys())}
+ if item_prop is not None:
+ item_prop = {key: item_prop[key] for key in sorted(item_prop.keys())}
+ item_repr = repr((item_op, item_event, item_prop))
+ unique.setdefault(item_repr, []).append(i)
+ for key, value in unique.items():
+ if len(value) > 1:
+ error_text.append("\"%s\" %r indices %r for item %r" % (km_idname, km_args, value, key))
+ return "\n".join(error_text)
+
+
def main() -> None:
import os
import sys
@@ -185,38 +277,64 @@ def main() -> None:
presets = keyconfig_preset_scan()
for filepath in presets:
name_only = os.path.splitext(os.path.basename(filepath))[0]
-
- print("KeyMap Validate:", name_only, end=" ... ")
-
- data_orig = keyconfig_activate_and_extract_data(filepath, relaxed=relaxed)
-
- with tempfile.TemporaryDirectory() as dir_temp:
- filepath_temp = os.path.join(dir_temp, name_only + ".test.py")
- bpy.ops.preferences.keyconfig_export(filepath=filepath_temp, all=True)
- data_reimport = keyconfig_activate_and_extract_data(filepath_temp, relaxed=relaxed)
-
- # Comparing a pretty printed string tends to give more useful
- # text output compared to the data-structure. Both will work.
- if (cmp_message := report_humanly_readable_difference(
- pprint.pformat(data_orig, indent=0, width=120),
- pprint.pformat(data_reimport, indent=0, width=120),
- )):
- print("FAILED!")
- sys.stdout.write((
- "Keymap %s has inconsistency on re-importing:\n"
- " %r"
- ) % (filepath, cmp_message))
- has_error = True
- else:
- print("OK!")
-
- if WRITE_OUTPUT_DIR:
- name_only_temp = os.path.join(WRITE_OUTPUT_DIR, name_only)
- print("Writing data to:", name_only_temp + ".*.py")
- with open(name_only_temp + ".orig.py", 'w') as fh:
- fh.write(pprint.pformat(data_orig, indent=0, width=120))
- with open(name_only_temp + ".rewrite.py", 'w') as fh:
- fh.write(pprint.pformat(data_reimport, indent=0, width=120))
+ for config in PRESET_PREFS.get(name_only, ((),)):
+ name_only_with_config = name_only + keyconfig_config_as_filename_component(config)
+ print("KeyMap Validate:", name_only_with_config, end=" ... ")
+ data_orig = keyconfig_activate_and_extract_data(
+ filepath,
+ relaxed=relaxed,
+ config=config,
+ )
+
+ with tempfile.TemporaryDirectory() as dir_temp:
+ filepath_temp = os.path.join(
+ dir_temp,
+ name_only_with_config + ".test" + ".py",
+ )
+
+ bpy.ops.preferences.keyconfig_export(filepath=filepath_temp, all=True)
+ data_reimport = keyconfig_activate_and_extract_data(
+ filepath_temp,
+ relaxed=relaxed,
+ # No configuration supported when loading exported key-maps.
+ config=(),
+ )
+
+ # Comparing a pretty printed string tends to give more useful
+ # text output compared to the data-structure. Both will work.
+ if (cmp_message := report_humanly_readable_difference(
+ pprint.pformat(data_orig, indent=0, width=120),
+ pprint.pformat(data_reimport, indent=0, width=120),
+ )):
+ error_text_consistency = "Keymap %s has inconsistency on re-importing." % cmp_message
+ else:
+ error_text_consistency = ""
+
+ # Perform an additional sanity check:
+ # That there are no identical key-map items.
+ if name_only not in ALLOW_DUPLICATES:
+ error_text_duplicates = keyconfig_report_duplicates(data_orig)
+ else:
+ error_text_duplicates = ""
+
+ if error_text_consistency or error_text_duplicates:
+ print("FAILED!")
+ print("%r has errors!" % filepath)
+ if error_text_consistency:
+ print(error_text_consistency)
+ if error_text_duplicates:
+ print(error_text_duplicates)
+ else:
+ print("OK!")
+
+ if WRITE_OUTPUT_DIR:
+ os.makedirs(WRITE_OUTPUT_DIR, exist_ok=True)
+ name_only_temp = os.path.join(WRITE_OUTPUT_DIR, name_only_with_config)
+ print("Writing data to:", name_only_temp + ".*.py")
+ with open(name_only_temp + ".orig.py", 'w') as fh:
+ fh.write(pprint.pformat(data_orig, indent=0, width=120))
+ with open(name_only_temp + ".rewrite.py", 'w') as fh:
+ fh.write(pprint.pformat(data_reimport, indent=0, width=120))
if has_error:
sys.exit(1)