diff --git a/blender_cloud/attract/__init__.py b/blender_cloud/attract/__init__.py index 86e427f..444516b 100644 --- a/blender_cloud/attract/__init__.py +++ b/blender_cloud/attract/__init__.py @@ -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' diff --git a/blender_cloud/blender.py b/blender_cloud/blender.py index bef4bbf..6940323 100644 --- a/blender_cloud/blender.py +++ b/blender_cloud/blender.py @@ -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' diff --git a/blender_cloud/compatibility.py b/blender_cloud/compatibility.py new file mode 100644 index 0000000..c89cd03 --- /dev/null +++ b/blender_cloud/compatibility.py @@ -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} diff --git a/blender_cloud/flamenco/__init__.py b/blender_cloud/flamenco/__init__.py index 0559ed6..4d81326 100644 --- a/blender_cloud/flamenco/__init__.py +++ b/blender_cloud/flamenco/__init__.py @@ -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, diff --git a/blender_cloud/image_sharing.py b/blender_cloud/image_sharing.py index b64cdd4..096c21b 100644 --- a/blender_cloud/image_sharing.py +++ b/blender_cloud/image_sharing.py @@ -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): diff --git a/blender_cloud/settings_sync.py b/blender_cloud/settings_sync.py index 4207c04..b678da0 100644 --- a/blender_cloud/settings_sync.py +++ b/blender_cloud/settings_sync.py @@ -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): diff --git a/blender_cloud/texture_browser/__init__.py b/blender_cloud/texture_browser/__init__.py index 56673b6..94994c3 100644 --- a/blender_cloud/texture_browser/__init__.py +++ b/blender_cloud/texture_browser/__init__.py @@ -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='') diff --git a/blender_cloud/utils.py b/blender_cloud/utils.py index 523508a..c454af9 100644 --- a/blender_cloud/utils.py +++ b/blender_cloud/utils.py @@ -94,7 +94,10 @@ def pyside_cache(propname): result = wrapped(self, context) return result finally: - rna_type, rna_info = getattr(self.bl_rna, propname) + 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 return decorator