Convert property definitions from assignment to annotations on Blender 2.80+

The properties are still declared in the Python 3.5 compatible assignment
notation, and a class decorator that converts those to class annotations
as preferred by Blender 2.80.
This commit is contained in:
Sybren A. Stüvel 2019-10-10 10:23:40 +02:00
parent 28f68c6fbf
commit 23b1f7de7d
8 changed files with 114 additions and 42 deletions

View File

@ -47,6 +47,7 @@ if "bpy" in locals():
pillar = importlib.reload(pillar)
async_loop = importlib.reload(async_loop)
blender = importlib.reload(blender)
compatibility = importlib.reload(compatibility)
else:
import bpy
@ -54,7 +55,7 @@ else:
from . import draw_27 as draw
else:
from . import draw
from .. import pillar, async_loop, blender
from .. import pillar, async_loop, blender, compatibility
import bpy
import pillarsdk
@ -252,7 +253,7 @@ class ATTRACT_PT_tools(AttractPollMixin, Panel):
icon='RENDER_STILL')
# Group more dangerous operations.
dangerous_sub = layout.split(**blender.factor(0.6), align=True)
dangerous_sub = layout.split(**compatibility.factor(0.6), align=True)
dangerous_sub.operator('attract.strip_unlink',
text='Unlink %s' % noun,
icon='PANEL_CLOSE')
@ -408,6 +409,7 @@ class ATTRACT_OT_shot_fetch_update(AttractOperatorMixin, Operator):
return {'FINISHED'}
@compatibility.convert_properties
class ATTRACT_OT_shot_relink(AttractOperatorMixin, Operator):
bl_idname = "attract.shot_relink"
bl_label = "Relink With Attract"
@ -477,6 +479,7 @@ class ATTRACT_OT_shot_open_in_browser(AttractOperatorMixin, Operator):
return {'FINISHED'}
@compatibility.convert_properties
class ATTRACT_OT_shot_delete(AttractOperatorMixin, Operator):
bl_idname = 'attract.shot_delete'
bl_label = 'Delete Shot'
@ -915,6 +918,7 @@ class ATTRACT_OT_copy_id_to_clipboard(AttractOperatorMixin, Operator):
return {'FINISHED'}
@compatibility.convert_properties
class ATTRACT_OT_project_open_in_browser(Operator):
bl_idname = 'attract.project_open_in_browser'
bl_label = 'Open Project in Browser'

View File

@ -30,7 +30,7 @@ from bpy.types import AddonPreferences, Operator, WindowManager, Scene, Property
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty, IntProperty
import rna_prop_ui
from . import pillar, async_loop, flamenco, project_specific
from . import compatibility, pillar, async_loop, flamenco, project_specific
from .utils import pyside_cache, redraw
PILLAR_WEB_SERVER_URL = os.environ.get('BCLOUD_SERVER', 'https://cloud.blender.org/')
@ -40,23 +40,6 @@ ADDON_NAME = 'blender_cloud'
log = logging.getLogger(__name__)
icons = None
if bpy.app.version < (2, 80):
SYNC_SELECT_VERSION_ICON = 'DOTSDOWN'
else:
SYNC_SELECT_VERSION_ICON = 'DOWNARROW_HLT'
@functools.lru_cache()
def factor(factor: float) -> dict:
"""Construct keyword argument for UILayout.split().
On Blender 2.8 this returns {'factor': factor}, and on earlier Blenders it returns
{'percentage': factor}.
"""
if bpy.app.version < (2, 80, 0):
return {'percentage': factor}
return {'factor': factor}
@pyside_cache('version')
def blender_syncable_versions(self, context):
@ -69,6 +52,7 @@ def blender_syncable_versions(self, context):
return [(v, v, '') for v in versions]
@compatibility.convert_properties
class SyncStatusProperties(PropertyGroup):
status = EnumProperty(
items=[
@ -156,6 +140,7 @@ def project_extensions(project_id) -> set:
return set(proj.get('enabled_for', ()))
@compatibility.convert_properties
class BlenderCloudProjectGroup(PropertyGroup):
status = EnumProperty(
items=[
@ -185,6 +170,7 @@ class BlenderCloudProjectGroup(PropertyGroup):
project_specific.handle_project_update()
@compatibility.convert_properties
class BlenderCloudPreferences(AddonPreferences):
bl_idname = ADDON_NAME
@ -331,7 +317,7 @@ class BlenderCloudPreferences(AddonPreferences):
bss = context.window_manager.blender_sync_status
bsync_box = layout.box()
bsync_box.enabled = msg_icon != 'ERROR'
row = bsync_box.row().split(**factor(0.33))
row = bsync_box.row().split(**compatibility.factor(0.33))
row.label(text='Blender Sync with Blender Cloud', icon_value=icon('CLOUD'))
icon_for_level = {
@ -374,7 +360,7 @@ class BlenderCloudPreferences(AddonPreferences):
layout.enabled = bss.status in {'NONE', 'IDLE'}
buttons = layout.column()
row_buttons = buttons.row().split(**factor(0.5))
row_buttons = buttons.row().split(**compatibility.factor(0.5))
row_push = row_buttons.row()
row_pull = row_buttons.row(align=True)
@ -396,7 +382,7 @@ class BlenderCloudPreferences(AddonPreferences):
props.blender_version = bss.version
row_pull.operator('pillar.sync',
text='',
icon=SYNC_SELECT_VERSION_ICON).action = 'SELECT'
icon=compatibility.SYNC_SELECT_VERSION_ICON).action = 'SELECT'
else:
row_pull.label(text='Cloud Sync is running.')
@ -444,7 +430,7 @@ class BlenderCloudPreferences(AddonPreferences):
header_row = flamenco_box.row(align=True)
header_row.label(text='Flamenco:', icon_value=icon('CLOUD'))
manager_split = flamenco_box.split(**factor(0.32), align=True)
manager_split = flamenco_box.split(**compatibility.factor(0.32), align=True)
manager_split.label(text='Manager:')
manager_box = manager_split.row(align=True)
@ -461,7 +447,7 @@ class BlenderCloudPreferences(AddonPreferences):
else:
manager_box.label(text='Fetching available managers.')
path_split = flamenco_box.split(**factor(0.32), align=True)
path_split = flamenco_box.split(**compatibility.factor(0.32), align=True)
path_split.label(text='Job File Path:')
path_box = path_split.row(align=True)
path_box.prop(self, 'flamenco_job_file_path', text='')
@ -469,7 +455,7 @@ class BlenderCloudPreferences(AddonPreferences):
props.path = self.flamenco_job_file_path
job_output_box = flamenco_box.column(align=True)
path_split = job_output_box.split(**factor(0.32), align=True)
path_split = job_output_box.split(**compatibility.factor(0.32), align=True)
path_split.label(text='Job Output Path:')
path_box = path_split.row(align=True)
path_box.prop(self, 'flamenco_job_output_path', text='')
@ -477,7 +463,7 @@ class BlenderCloudPreferences(AddonPreferences):
props.path = self.flamenco_job_output_path
job_output_box.prop(self, 'flamenco_exclude_filter')
prop_split = job_output_box.split(**factor(0.32), align=True)
prop_split = job_output_box.split(**compatibility.factor(0.32), align=True)
prop_split.label(text='Strip Components:')
prop_split.prop(self, 'flamenco_job_output_strip_components', text='')
@ -561,6 +547,7 @@ class PILLAR_OT_subscribe(Operator):
return {'FINISHED'}
@compatibility.convert_properties
class PILLAR_OT_project_open_in_browser(Operator):
bl_idname = 'pillar.project_open_in_browser'
bl_label = 'Open in Browser'

View File

@ -0,0 +1,70 @@
"""Compatibility functions to support Blender 2.79 and 2.80+ in one code base."""
import functools
import bpy
if bpy.app.version < (2, 80):
SYNC_SELECT_VERSION_ICON = 'DOTSDOWN'
else:
SYNC_SELECT_VERSION_ICON = 'DOWNARROW_HLT'
# Get references to all property definition functions in bpy.props,
# so that they can be used to replace 'x = IntProperty()' to 'x: IntProperty()'
# dynamically when working on Blender 2.80+
__all_prop_funcs = {
getattr(bpy.props, propname)
for propname in dir(bpy.props)
if propname.endswith('Property')
}
def convert_properties(class_):
"""Class decorator to avoid warnings in Blender 2.80+
This decorator replaces property definitions like this:
someprop = bpy.props.IntProperty()
to annotations, as introduced in Blender 2.80:
someprop: bpy.props.IntProperty()
No-op if running on Blender 2.79 or older.
"""
if bpy.app.version < (2, 80):
return class_
if not hasattr(class_, '__annotations__'):
class_.__annotations__ = {}
attrs_to_delete = []
for name, value in class_.__dict__.items():
if not isinstance(value, tuple) or len(value) != 2:
continue
prop_func, kwargs = value
if prop_func not in __all_prop_funcs:
continue
# This is a property definition, replace it with annotation.
attrs_to_delete.append(name)
class_.__annotations__[name] = value
for attr_name in attrs_to_delete:
delattr(class_, attr_name)
return class_
@functools.lru_cache()
def factor(factor: float) -> dict:
"""Construct keyword argument for UILayout.split().
On Blender 2.8 this returns {'factor': factor}, and on earlier Blenders it returns
{'percentage': factor}.
"""
if bpy.app.version < (2, 80, 0):
return {'percentage': factor}
return {'factor': factor}

View File

@ -34,12 +34,13 @@ if "bpy" in locals():
bat_interface = importlib.reload(bat_interface)
sdk = importlib.reload(sdk)
blender = importlib.reload(blender)
compatibility = importlib.reload(compatibility)
except NameError:
from . import bat_interface, sdk
from .. import blender
from .. import blender, compatibility
else:
from . import bat_interface, sdk
from .. import blender
from .. import blender, compatibility
import bpy
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
@ -138,6 +139,7 @@ def silently_quit_blender():
bpy.ops.wm.quit_blender()
@compatibility.convert_properties
class FlamencoManagerGroup(PropertyGroup):
manager = EnumProperty(
items=available_managers,
@ -238,6 +240,7 @@ def guess_output_file_extension(output_format: str, scene) -> str:
return '.' + container.lower()
@compatibility.convert_properties
class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
pillar.AuthenticatedPillarOperatorMixin,
FlamencoPollMixin,
@ -729,6 +732,7 @@ class FLAMENCO_OT_abort(Operator, FlamencoPollMixin):
return {'FINISHED'}
@compatibility.convert_properties
class FLAMENCO_OT_explore_file_path(FlamencoPollMixin,
Operator):
"""Opens the Flamenco job storage path in a file explorer.
@ -796,6 +800,7 @@ class FLAMENCO_OT_disable_output_path_override(Operator):
return {'FINISHED'}
@compatibility.convert_properties
class FLAMENCO_OT_set_recommended_sample_cap(Operator):
bl_idname = 'flamenco.set_recommended_sample_cap'
bl_label = 'Set Recommended Maximum Sample Count'
@ -962,7 +967,7 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
prefs = preferences()
labeled_row = layout.split(**blender.factor(0.25), align=True)
labeled_row = layout.split(**compatibility.factor(0.25), align=True)
labeled_row.label(text='Manager:')
prop_btn_row = labeled_row.row(align=True)
@ -980,7 +985,7 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
else:
prop_btn_row.label(text='Fetching available managers.')
labeled_row = layout.split(**blender.factor(0.25), align=True)
labeled_row = layout.split(**compatibility.factor(0.25), align=True)
labeled_row.label(text='Job Type:')
labeled_row.prop(context.scene, 'flamenco_render_job_type', text='')
@ -1005,7 +1010,7 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
sample_count = scene_sample_count(context.scene)
recommended_cap = sample_count // 4
split = box.split(**blender.factor(0.4))
split = box.split(**compatibility.factor(0.4))
split.label(text='Total Sample Count: %d' % sample_count)
props = split.operator('flamenco.set_recommended_sample_cap',
text='Recommended Max Samples per Task: %d' % recommended_cap)
@ -1019,7 +1024,7 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
else:
box.prop(context.scene, 'flamenco_render_fchunk_size')
labeled_row = layout.split(**blender.factor(0.25), align=True)
labeled_row = layout.split(**compatibility.factor(0.25), align=True)
labeled_row.label(text='Frame Range:')
prop_btn_row = labeled_row.row(align=True)
prop_btn_row.prop(context.scene, 'flamenco_render_frame_range', text='')
@ -1030,7 +1035,7 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
paths_layout = layout.column(align=True)
labeled_row = paths_layout.split(**blender.factor(0.25), align=True)
labeled_row = paths_layout.split(**compatibility.factor(0.25), align=True)
labeled_row.label(text='Storage:')
prop_btn_row = labeled_row.row(align=True)
prop_btn_row.label(text=prefs.flamenco_job_file_path)
@ -1043,7 +1048,7 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
paths_layout.label(text='Unable to render with Flamenco, outside of project directory.')
return
labeled_row = paths_layout.split(**blender.factor(0.25), align=True)
labeled_row = paths_layout.split(**compatibility.factor(0.25), align=True)
labeled_row.label(text='Output:')
prop_btn_row = labeled_row.row(align=True)
@ -1062,7 +1067,7 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
props.path = str(render_output.parent)
if context.scene.flamenco_do_override_output_path:
labeled_row = paths_layout.split(**blender.factor(0.25), align=True)
labeled_row = paths_layout.split(**compatibility.factor(0.25), align=True)
labeled_row.label(text='Effective Output Path:')
labeled_row.label(text=str(render_output))
@ -1072,7 +1077,7 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
flamenco_status = context.window_manager.flamenco_status
if flamenco_status in {'IDLE', 'ABORTED', 'DONE'}:
if prefs.flamenco_show_quit_after_submit_button:
ui = layout.split(**blender.factor(0.75), align=True)
ui = layout.split(**compatibility.factor(0.75), align=True)
else:
ui = layout
ui.operator(FLAMENCO_OT_render.bl_idname,

View File

@ -25,7 +25,7 @@ import bpy
import pillarsdk
from pillarsdk import exceptions as sdk_exceptions
from .pillar import pillar_call
from . import async_loop, pillar, home_project, blender
from . import async_loop, compatibility, pillar, home_project, blender
REQUIRES_ROLES_FOR_IMAGE_SHARING = {'subscriber', 'demo'}
IMAGE_SHARING_GROUP_NODE_NAME = 'Image sharing'
@ -53,6 +53,7 @@ async def find_image_sharing_group_id(home_project_id, user_id):
return share_group['_id']
@compatibility.convert_properties
class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
async_loop.AsyncModalOperatorMixin,
bpy.types.Operator):

View File

@ -35,7 +35,7 @@ import asyncio
import pillarsdk
from pillarsdk import exceptions as sdk_exceptions
from .pillar import pillar_call
from . import async_loop, blender, pillar, cache, blendfile, home_project
from . import async_loop, blender, compatibility, pillar, cache, blendfile, home_project
SETTINGS_FILES_TO_UPLOAD = ['userpref.blend', 'startup.blend']
@ -196,6 +196,7 @@ async def available_blender_versions(home_project_id: str, user_id: str) -> list
# noinspection PyAttributeOutsideInit
@compatibility.convert_properties
class PILLAR_OT_sync(pillar.PillarOperatorMixin,
async_loop.AsyncModalOperatorMixin,
bpy.types.Operator):

View File

@ -26,7 +26,7 @@ import bpy
import bgl
import pillarsdk
from .. import async_loop, pillar, cache, blender, utils
from .. import async_loop, compatibility, pillar, cache, blender, utils
from . import menu_item as menu_item_mod # so that we can have menu items called 'menu_item'
from . import nodes
@ -648,6 +648,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
self.scroll_offset_target = self.scroll_offset = 0
@compatibility.convert_properties
class PILLAR_OT_switch_hdri(pillar.PillarOperatorMixin,
async_loop.AsyncModalOperatorMixin,
bpy.types.Operator):
@ -776,7 +777,7 @@ def _hdri_download_panel(self, current_image):
current_image.name)
return
row = self.layout.row(align=True).split(**blender.factor(0.3))
row = self.layout.row(align=True).split(**compatibility.factor(0.3))
row.label(text='HDRi', icon_value=blender.icon('CLOUD'))
row.prop(current_image, 'hdri_variation', text='')

View File

@ -94,6 +94,9 @@ def pyside_cache(propname):
result = wrapped(self, context)
return result
finally:
try:
rna_type, rna_info = self.bl_rna.__annotations__[propname]
except AttributeError:
rna_type, rna_info = getattr(self.bl_rna, propname)
rna_info['_cached_result'] = result
return wrapper