Addon: cleanup, split propgroup generator from job type code

This commit is contained in:
Sybren A. Stüvel 2022-03-14 14:39:10 +01:00
parent 2e78e00a0b
commit ad3750dfbe
3 changed files with 220 additions and 180 deletions

View File

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

View File

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

View File

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