From ad3750dfbed8bce1c405cbb4c8393cf0d16a1187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Mon, 14 Mar 2022 14:39:10 +0100 Subject: [PATCH] Addon: cleanup, split propgroup generator from job type code --- addon/flamenco/job_types.py | 191 ++---------------------- addon/flamenco/job_types_propgroup.py | 205 ++++++++++++++++++++++++++ addon/test_jobtypes.py | 4 +- 3 files changed, 220 insertions(+), 180 deletions(-) create mode 100644 addon/flamenco/job_types_propgroup.py diff --git a/addon/flamenco/job_types.py b/addon/flamenco/job_types.py index 8ee59f3d..1aecc8de 100644 --- a/addon/flamenco/job_types.py +++ b/addon/flamenco/job_types.py @@ -6,37 +6,10 @@ from typing import TYPE_CHECKING, Callable, Optional, Union import bpy +from . import job_types_propgroup + _log = logging.getLogger(__name__) - -class JobTypePropertyGroup: - @classmethod - def register_property_group(cls): - bpy.utils.register_class(cls) - - @classmethod - def unregister_property_group(cls): - bpy.utils.unregister_class(cls) - - -# Mapping from AvailableJobType.setting.type to a callable that converts a value -# to the appropriate type. This is necessary due to the ambiguity between floats -# and ints in JavaScript (and thus JSON). -_value_coerce = { - "bool": bool, - "string": str, - "int32": int, - "float": float, -} - -_prop_types = { - "bool": bpy.props.BoolProperty, - "string": bpy.props.StringProperty, - "int32": bpy.props.IntProperty, - "float": bpy.props.FloatProperty, -} - - if TYPE_CHECKING: from flamenco.manager.models import AvailableJobType, SubmittedJob, JobSettings @@ -51,7 +24,7 @@ _job_type_enum_items: list[ Union[tuple[str, str, str], tuple[str, str, str, int, int]] ] = [] -_selected_job_type_propgroup: Optional[JobTypePropertyGroup] = None +_selected_job_type_propgroup: Optional[job_types_propgroup.JobTypePropertyGroup] = None def fetch_available_job_types(api_client): @@ -99,13 +72,16 @@ def update_job_type_properties(scene: bpy.types.Scene) -> None: from flamenco.manager.model.available_job_type import AvailableJobType job_type = active_job_type(scene) + _clear_job_type_propgroup() + + if job_type is None: + return + assert isinstance(job_type, AvailableJobType), "did not expect type %r" % type( job_type ) - _clear_job_type_propgroup() - - pg = generate_property_group(job_type) + pg = job_types_propgroup.generate(job_type) pg.register_property_group() _selected_job_type_propgroup = pg @@ -199,151 +175,6 @@ def active_job_type(scene: bpy.types.Scene) -> Optional[_AvailableJobType]: return None -def generate_property_group(job_type): - """Create a PropertyGroup for the job type. - - Does not register the property group. - """ - from flamenco.manager.model.available_job_type import AvailableJobType - - assert isinstance(job_type, AvailableJobType) - - classname = _job_type_to_class_name(job_type.name) - - pg_type = type( - classname, - (JobTypePropertyGroup, bpy.types.PropertyGroup), # Base classes. - { # Class attributes. - "job_type": job_type, - }, - ) - pg_type.__annotations__ = {} - - print(f"\033[38;5;214m{job_type.label}\033[0m ({job_type.name})") - for setting in job_type.settings: - prop = _create_property(job_type, setting) - pg_type.__annotations__[setting.key] = prop - - assert issubclass(pg_type, JobTypePropertyGroup), "did not expect type %r" % type( - pg_type - ) - - from pprint import pprint - - print(pg_type) - pprint(pg_type.__annotations__) - - return pg_type - - -def _create_property(job_type, setting): - from flamenco.manager.model.available_job_setting import AvailableJobSetting - from flamenco.manager.model_utils import ModelSimple - - assert isinstance(setting, AvailableJobSetting) - - print(f" - {setting.key:23} type: {setting.type!r:10}", end="") - - # Special case: a string property with 'choices' setting. This should translate to an EnumProperty - prop_type, prop_kwargs = _find_prop_type(job_type, setting) - - assert isinstance(setting.type, ModelSimple) - value_coerce = _value_coerce[setting.type.to_str()] - _set_if_available(prop_kwargs, setting, "description") - _set_if_available(prop_kwargs, setting, "default", transform=value_coerce) - _set_if_available(prop_kwargs, setting, "subtype", transform=_transform_subtype) - print() - - prop_name = _job_setting_key_to_label(setting.key) - prop = prop_type(name=prop_name, **prop_kwargs) - return prop - - -def _find_prop_type(job_type, setting): - # The special case is a 'string' property with 'choices' setting, which - # should translate to an EnumProperty. All others just map to a simple - # bpy.props type. - - setting_type = setting.type.to_str() - - if "choices" not in setting: - return _prop_types[setting_type], {} - - if setting_type != "string": - # There was a 'choices' key, but not for a supported type. Ignore the - # choices but complain about it. - _log.warn( - "job type %r, setting %r: only string choices are supported, but property is of type %s", - job_type.name, - setting.key, - setting_type, - ) - return _prop_types[setting_type], {} - - choices = setting.choices - enum_items = [(choice, choice, "") for choice in choices] - return bpy.props.EnumProperty, {"items": enum_items} - - -def _transform_subtype(subtype: object) -> str: - uppercase = str(subtype).upper() - if uppercase == "HASHED_FILE_PATH": - # Flamenco has a concept of 'hashed file path' subtype, but Blender does not. - return "FILE_PATH" - return uppercase - - -def _job_type_to_class_name(job_type_name: str) -> str: - """Change 'job-type-name' to 'JobTypeName'. - - >>> _job_type_to_class_name('job-type-name') - 'JobTypeName' - """ - return job_type_name.title().replace("-", "") - - -def _job_setting_key_to_label(setting_key: str) -> str: - """Change 'some_setting_key' to 'Some Setting Key'. - - >>> _job_setting_key_to_label('some_setting_key') - 'Some Setting Key' - """ - return setting_key.title().replace("_", " ") - - -def _set_if_available( - some_dict: dict[object, object], - setting: object, - key: str, - transform: Optional[Callable[[object], object]] = None, -) -> None: - """some_dict[key] = setting.key, if that key is available. - - >>> class Setting: - ... pass - >>> setting = Setting() - >>> setting.exists = 47 - >>> d = {} - >>> _set_if_available(d, setting, "exists") - >>> _set_if_available(d, setting, "other") - >>> d - {'exists': 47} - >>> d = {} - >>> _set_if_available(d, setting, "exists", transform=lambda v: str(v)) - >>> d - {'exists': '47'} - """ - try: - value = getattr(setting, key) - except AttributeError: - return - - if transform is None: - some_dict[key] = value - else: - some_dict[key] = transform(value) - - def _get_job_types_enum_items(dummy1, dummy2): return _job_type_enum_items @@ -362,6 +193,10 @@ def register() -> None: update=_update_job_type, ) + bpy.types.Scene.flamenco_available_job_types_json = bpy.props.StringProperty( + name="Available Job Types", + ) + def unregister() -> None: del bpy.types.Scene.flamenco_job_type diff --git a/addon/flamenco/job_types_propgroup.py b/addon/flamenco/job_types_propgroup.py new file mode 100644 index 00000000..6a6947c4 --- /dev/null +++ b/addon/flamenco/job_types_propgroup.py @@ -0,0 +1,205 @@ +"""Flamenco Job Type to bpy.props.PropertyGroup conversion.""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +import logging +from typing import TYPE_CHECKING, Callable, Optional, Any + +import bpy + +_log = logging.getLogger(__name__) + +if TYPE_CHECKING: + from flamenco.manager.models import AvailableJobType, AvailableJobSetting +else: + AvailableJobType = object + AvailableJobSetting = object + + +class JobTypePropertyGroup: + """Mix-in class for PropertyGroups for Flamenco Job Types. + + Use `generate(job_type: AvailableJobType)` to create such a subclass. + """ + + job_type: AvailableJobType + """The job type passed to `generate(job_type)`.""" + + @classmethod + def register_property_group(cls): + bpy.utils.register_class(cls) + + @classmethod + def unregister_property_group(cls): + bpy.utils.unregister_class(cls) + + +# Mapping from AvailableJobType.setting.type to a callable that converts a value +# to the appropriate type. This is necessary due to the ambiguity between floats +# and ints in JavaScript (and thus JSON). +_value_coerce = { + "bool": bool, + "string": str, + "int32": int, + "float": float, +} + +_prop_types = { + "bool": bpy.props.BoolProperty, + "string": bpy.props.StringProperty, + "int32": bpy.props.IntProperty, + "float": bpy.props.FloatProperty, +} + + +def generate(job_type: AvailableJobType) -> type[JobTypePropertyGroup]: + """Create a PropertyGroup for the job type. + + Does not register the property group. + """ + from flamenco.manager.model.available_job_type import AvailableJobType + + assert isinstance(job_type, AvailableJobType) + + classname = _job_type_to_class_name(job_type.name) + + pg_type = type( + classname, + (JobTypePropertyGroup, bpy.types.PropertyGroup), # Base classes. + { # Class attributes. + "job_type": job_type, + }, + ) + pg_type.__annotations__ = {} + + print(f"\033[38;5;214m{job_type.label}\033[0m ({job_type.name})") + for setting in job_type.settings: + prop = _create_property(job_type, setting) + pg_type.__annotations__[setting.key] = prop + + assert issubclass(pg_type, JobTypePropertyGroup), "did not expect type %r" % type( + pg_type + ) + + from pprint import pprint + + print(pg_type) + pprint(pg_type.__annotations__) + + return pg_type + + +def _create_property(job_type: AvailableJobType, setting: AvailableJobSetting) -> Any: + """Create a bpy.props property for the given job setting. + + Depending on the setting, will be a StringProperty, EnumProperty, FloatProperty, etc. + """ + from flamenco.manager.model.available_job_setting import AvailableJobSetting + from flamenco.manager.model_utils import ModelSimple + + assert isinstance(setting, AvailableJobSetting) + + print(f" - {setting.key:23} type: {setting.type!r:10}", end="") + + # Special case: a string property with 'choices' setting. This should translate to an EnumProperty + prop_type, prop_kwargs = _find_prop_type(job_type, setting) + + assert isinstance(setting.type, ModelSimple) + value_coerce = _value_coerce[setting.type.to_str()] + _set_if_available(prop_kwargs, setting, "description") + _set_if_available(prop_kwargs, setting, "default", transform=value_coerce) + _set_if_available(prop_kwargs, setting, "subtype", transform=_transform_subtype) + print() + + prop_name = _job_setting_key_to_label(setting.key) + prop = prop_type(name=prop_name, **prop_kwargs) + return prop + + +def _find_prop_type( + job_type: AvailableJobType, setting: AvailableJobSetting +) -> tuple[Any, dict[str, Any]]: + """Return a tuple (bpy.props.XxxProperty, kwargs for construction).""" + + # The special case is a 'string' property with 'choices' setting, which + # should translate to an EnumProperty. All others just map to a simple + # bpy.props type. + + setting_type = setting.type.to_str() + + if "choices" not in setting: + return _prop_types[setting_type], {} + + if setting_type != "string": + # There was a 'choices' key, but not for a supported type. Ignore the + # choices but complain about it. + _log.warn( + "job type %r, setting %r: only string choices are supported, but property is of type %s", + job_type.name, + setting.key, + setting_type, + ) + return _prop_types[setting_type], {} + + choices = setting.choices + enum_items = [(choice, choice, "") for choice in choices] + return bpy.props.EnumProperty, {"items": enum_items} + + +def _transform_subtype(subtype: object) -> str: + uppercase = str(subtype).upper() + if uppercase == "HASHED_FILE_PATH": + # Flamenco has a concept of 'hashed file path' subtype, but Blender does not. + return "FILE_PATH" + return uppercase + + +def _job_type_to_class_name(job_type_name: str) -> str: + """Change 'job-type-name' to 'JobTypeName'. + + >>> _job_type_to_class_name('job-type-name') + 'JobTypeName' + """ + return job_type_name.title().replace("-", "") + + +def _job_setting_key_to_label(setting_key: str) -> str: + """Change 'some_setting_key' to 'Some Setting Key'. + + >>> _job_setting_key_to_label('some_setting_key') + 'Some Setting Key' + """ + return setting_key.title().replace("_", " ") + + +def _set_if_available( + some_dict: dict[object, object], + setting: object, + key: str, + transform: Optional[Callable[[object], object]] = None, +) -> None: + """some_dict[key] = setting.key, if that key is available. + + >>> class Setting: + ... pass + >>> setting = Setting() + >>> setting.exists = 47 + >>> d = {} + >>> _set_if_available(d, setting, "exists") + >>> _set_if_available(d, setting, "other") + >>> d + {'exists': 47} + >>> d = {} + >>> _set_if_available(d, setting, "exists", transform=lambda v: str(v)) + >>> d + {'exists': '47'} + """ + try: + value = getattr(setting, key) + except AttributeError: + return + + if transform is None: + some_dict[key] = value + else: + some_dict[key] = transform(value) diff --git a/addon/test_jobtypes.py b/addon/test_jobtypes.py index 38219919..ca50cef1 100644 --- a/addon/test_jobtypes.py +++ b/addon/test_jobtypes.py @@ -8,7 +8,7 @@ sys.path.append(str(my_dir)) import atexit -from flamenco import dependencies, job_types +from flamenco import dependencies, job_types_propgroup as jt_propgroup dependencies.preload_modules() @@ -33,4 +33,4 @@ except flamenco.manager.ApiException as ex: raise SystemExit("Exception when calling JobsApi->fetch_job: %s" % ex) job_type = next(jt for jt in response.job_types if jt.name == "simple-blender-render") -pg = job_types.generate_property_group(job_type) +pg = jt_propgroup.generate(job_type)