Compare commits
28 Commits
version-1.
...
version-1.
Author | SHA1 | Date | |
---|---|---|---|
3ed5f2c187 | |||
0be3bf7f49 | |||
f207e14664 | |||
9932003400 | |||
e7035e6f0c | |||
014a36d24e | |||
068451a7aa | |||
56fb1ec3df | |||
e93094cb88 | |||
33718a1a35 | |||
db82dbe730 | |||
8d405330ee | |||
66ddc7b47b | |||
2fa8cb4054 | |||
e7b5c75046 | |||
1d93bd9e5e | |||
ac2d0c033c | |||
61fa63eb1d | |||
7022412889 | |||
b4f71745b0 | |||
1d41fce1ae | |||
e636fde4ce | |||
82a9dc5226 | |||
1f40915ac8 | |||
32693c0f64 | |||
c38748eb05 | |||
ac85bea111 | |||
7b5613ce77 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,5 +1,29 @@
|
||||
# Blender Cloud changelog
|
||||
|
||||
## Version 1.7.4 (2017-09-05)
|
||||
|
||||
- Fix [T52621](https://developer.blender.org/T52621): Fixed class name collision upon add-on
|
||||
registration. This is checked since Blender 2.79.
|
||||
- Fix [T48852](https://developer.blender.org/T48852): Screenshot no longer shows "Communicating with
|
||||
Blender Cloud".
|
||||
|
||||
|
||||
## Version 1.7.3 (2017-08-08)
|
||||
|
||||
- Default to scene frame range when no frame range is given.
|
||||
- Refuse to render on Flamenco before blend file is saved at least once.
|
||||
- Fixed some Windows-specific issues.
|
||||
|
||||
|
||||
## Version 1.7.2 (2017-06-22)
|
||||
|
||||
- Fixed compatibility with Blender 2.78c.
|
||||
|
||||
|
||||
## Version 1.7.1 (2017-06-13)
|
||||
|
||||
- Fixed asyncio issues on Windows
|
||||
|
||||
|
||||
## Version 1.7.0 (2017-06-09)
|
||||
|
||||
@@ -10,7 +34,7 @@
|
||||
|
||||
## Version 1.6.4 (2017-04-21)
|
||||
|
||||
- Added file exclusion filter for Flamenco. A filter like "*.abc;*.mkv;*.mov" can be
|
||||
- Added file exclusion filter for Flamenco. A filter like `*.abc;*.mkv;*.mov` can be
|
||||
used to prevent certain files from being copied to the job storage directory.
|
||||
Requires a Blender that is bundled with BAM 1.1.7 or newer.
|
||||
|
||||
|
@@ -21,7 +21,7 @@
|
||||
bl_info = {
|
||||
'name': 'Blender Cloud',
|
||||
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
|
||||
'version': (1, 7, 0),
|
||||
'version': (1, 7, 4),
|
||||
'blender': (2, 77, 0),
|
||||
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
|
||||
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '
|
||||
|
@@ -33,17 +33,12 @@ _loop_kicking_operator_running = False
|
||||
|
||||
|
||||
def setup_asyncio_executor():
|
||||
"""Sets up AsyncIO to run on a single thread.
|
||||
"""Sets up AsyncIO to run properly on each platform."""
|
||||
|
||||
This ensures that only one Pillar HTTP call is performed at the same time. Other
|
||||
calls that could be performed in parallel are queued, and thus we can
|
||||
reliably cancel them.
|
||||
"""
|
||||
import sys
|
||||
|
||||
executor = concurrent.futures.ThreadPoolExecutor()
|
||||
|
||||
if sys.platform == 'win32':
|
||||
asyncio.get_event_loop().close()
|
||||
# On Windows, the default event loop is SelectorEventLoop, which does
|
||||
# not support subprocesses. ProactorEventLoop should be used instead.
|
||||
# Source: https://docs.python.org/3/library/asyncio-subprocess.html
|
||||
@@ -51,9 +46,15 @@ def setup_asyncio_executor():
|
||||
asyncio.set_event_loop(loop)
|
||||
else:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
executor = concurrent.futures.ThreadPoolExecutor(max_workers=10)
|
||||
loop.set_default_executor(executor)
|
||||
# loop.set_debug(True)
|
||||
|
||||
from . import pillar
|
||||
# No more than this many Pillar calls should be made simultaneously
|
||||
pillar.pillar_semaphore = asyncio.Semaphore(3, loop=loop)
|
||||
|
||||
|
||||
def kick_async_loop(*args) -> bool:
|
||||
"""Performs a single iteration of the asyncio event loop.
|
||||
|
@@ -173,7 +173,7 @@ class AttractPollMixin:
|
||||
return attract_is_active
|
||||
|
||||
|
||||
class ToolsPanel(AttractPollMixin, Panel):
|
||||
class AttractToolsPanel(AttractPollMixin, Panel):
|
||||
bl_label = 'Attract'
|
||||
bl_space_type = 'SEQUENCE_EDITOR'
|
||||
bl_region_type = 'UI'
|
||||
@@ -974,7 +974,7 @@ def register():
|
||||
|
||||
bpy.types.SEQUENCER_PT_edit.append(draw_strip_movie_meta)
|
||||
|
||||
bpy.utils.register_class(ToolsPanel)
|
||||
bpy.utils.register_class(AttractToolsPanel)
|
||||
bpy.utils.register_class(AttractShotRelink)
|
||||
bpy.utils.register_class(AttractShotDelete)
|
||||
bpy.utils.register_class(AttractStripUnlink)
|
||||
|
@@ -32,8 +32,7 @@ import rna_prop_ui
|
||||
from . import pillar, async_loop, flamenco
|
||||
from .utils import pyside_cache, redraw
|
||||
|
||||
PILLAR_WEB_SERVER_URL = 'https://cloud.blender.org/'
|
||||
# PILLAR_WEB_SERVER_URL = 'http://pillar-web:5001/'
|
||||
PILLAR_WEB_SERVER_URL = os.environ.get('BCLOUD_SERVER', 'https://cloud.blender.org/')
|
||||
PILLAR_SERVER_URL = '%sapi/' % PILLAR_WEB_SERVER_URL
|
||||
|
||||
ADDON_NAME = 'blender_cloud'
|
||||
@@ -462,16 +461,7 @@ class BlenderCloudPreferences(AddonPreferences):
|
||||
path_box.prop(self, 'flamenco_job_output_path', text='')
|
||||
props = path_box.operator('flamenco.explore_file_path', text='', icon='DISK_DRIVE')
|
||||
props.path = self.flamenco_job_output_path
|
||||
|
||||
show_warning = bool(self.flamenco_exclude_filter and
|
||||
not bam_interface.bam_supports_exclude_option())
|
||||
job_output_box.alert = show_warning
|
||||
job_output_box.prop(self, 'flamenco_exclude_filter',
|
||||
icon='ERROR' if show_warning else 'NONE')
|
||||
if show_warning:
|
||||
job_output_box.label(
|
||||
text='Warning, the exclusion filter requires a newer version of Blender!')
|
||||
job_output_box.alert = False
|
||||
job_output_box.prop(self, 'flamenco_exclude_filter')
|
||||
|
||||
prop_split = job_output_box.split(0.32, align=True)
|
||||
prop_split.label('Strip Components:')
|
||||
|
@@ -20,11 +20,26 @@
|
||||
|
||||
The preferences are managed blender.py, the rest of the Flamenco-specific stuff is here.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path, PurePath
|
||||
import typing
|
||||
|
||||
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
|
||||
try:
|
||||
bam_interface = importlib.reload(bam_interface)
|
||||
sdk = importlib.reload(sdk)
|
||||
except NameError:
|
||||
from . import bam_interface, sdk
|
||||
else:
|
||||
from . import bam_interface, sdk
|
||||
|
||||
|
||||
import bpy
|
||||
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
|
||||
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty, IntProperty
|
||||
@@ -32,6 +47,7 @@ from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolPropert
|
||||
from .. import async_loop, pillar
|
||||
from ..utils import pyside_cache, redraw
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Global flag used to determine whether panels etc. can be drawn.
|
||||
@@ -136,6 +152,14 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
|
||||
log = logging.getLogger('%s.FLAMENCO_OT_render' % __name__)
|
||||
|
||||
async def async_execute(self, context):
|
||||
# Refuse to start if the file hasn't been saved. It's okay if
|
||||
# it's dirty, but we do need a filename and a location.
|
||||
if not os.path.exists(context.blend_data.filepath):
|
||||
self.report({'ERROR'}, 'Please save your Blend file before using '
|
||||
'the Blender Cloud addon.')
|
||||
self.quit()
|
||||
return
|
||||
|
||||
if not await self.authenticate(context):
|
||||
return
|
||||
|
||||
@@ -178,10 +202,12 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
|
||||
|
||||
# Create the job at Flamenco Server.
|
||||
context.window_manager.flamenco_status = 'COMMUNICATING'
|
||||
|
||||
frame_range = scene.flamenco_render_frame_range.strip() or scene_frame_range(context)
|
||||
settings = {'blender_cmd': '{blender}',
|
||||
'chunk_size': scene.flamenco_render_fchunk_size,
|
||||
'filepath': manager.replace_path(outfile),
|
||||
'frames': scene.flamenco_render_frame_range,
|
||||
'frames': frame_range,
|
||||
'render_output': manager.replace_path(render_output),
|
||||
}
|
||||
|
||||
@@ -301,7 +327,6 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
|
||||
|
||||
from datetime import datetime
|
||||
from ..blender import preferences
|
||||
from . import bam_interface
|
||||
|
||||
prefs = preferences()
|
||||
|
||||
@@ -335,6 +360,13 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
|
||||
return outfile, missing_sources
|
||||
|
||||
|
||||
def scene_frame_range(context) -> str:
|
||||
"""Returns the frame range string for the current scene."""
|
||||
|
||||
s = context.scene
|
||||
return '%i-%i' % (s.frame_start, s.frame_end)
|
||||
|
||||
|
||||
class FLAMENCO_OT_scene_to_frame_range(FlamencoPollMixin, Operator):
|
||||
"""Sets the scene frame range as the Flamenco render frame range."""
|
||||
bl_idname = 'flamenco.scene_to_frame_range'
|
||||
@@ -342,8 +374,7 @@ class FLAMENCO_OT_scene_to_frame_range(FlamencoPollMixin, Operator):
|
||||
bl_description = __doc__.rstrip('.')
|
||||
|
||||
def execute(self, context):
|
||||
s = context.scene
|
||||
s.flamenco_render_frame_range = '%i-%i' % (s.frame_start, s.frame_end)
|
||||
context.scene.flamenco_render_frame_range = scene_frame_range(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
@@ -359,14 +390,15 @@ class FLAMENCO_OT_copy_files(Operator,
|
||||
|
||||
async def async_execute(self, context):
|
||||
from pathlib import Path
|
||||
from . import bam_interface
|
||||
from ..blender import preferences
|
||||
|
||||
context.window_manager.flamenco_status = 'PACKING'
|
||||
exclusion_filter = preferences().flamenco_exclude_filter or None
|
||||
|
||||
missing_sources = await bam_interface.bam_copy(
|
||||
Path(context.blend_data.filepath),
|
||||
Path(preferences().flamenco_job_file_path),
|
||||
exclusion_filter
|
||||
)
|
||||
|
||||
if missing_sources:
|
||||
|
@@ -1,6 +1,5 @@
|
||||
"""BAM packing interface for Flamenco."""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import typing
|
||||
@@ -15,26 +14,26 @@ class CommandExecutionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
if 'bam_supports_exclude_option' in locals():
|
||||
locals()['bam_supports_exclude_option'].cache_clear()
|
||||
def wheel_pythonpath_278() -> str:
|
||||
"""Returns the value of a PYTHONPATH environment variable needed to run BAM from its wheel file.
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def bam_supports_exclude_option() -> bool:
|
||||
"""Returns True if the version of BAM bundled with Blender supports --exclude.
|
||||
|
||||
This feature was added to BAM 1.1.7, so we can do a simple version check.
|
||||
Workaround for Blender 2.78c not having io_blend_utils.pythonpath()
|
||||
"""
|
||||
|
||||
try:
|
||||
import io_blend_utils
|
||||
except ImportError:
|
||||
# If this happens, BAM won't work at all. However, this function can be called from
|
||||
# the GUI; by being a bit careful while importing, we avoid breaking Blender's GUI.
|
||||
log.exception('Error importing io_blend_utils module.')
|
||||
return False
|
||||
import os
|
||||
from ..wheels import wheel_filename
|
||||
|
||||
return io_blend_utils.bl_info['version'] >= (1, 1, 7)
|
||||
# Find the wheel to run.
|
||||
wheelpath = wheel_filename('blender_bam')
|
||||
|
||||
log.info('Using wheel %s to run BAM-Pack', wheelpath)
|
||||
|
||||
# Update the PYTHONPATH to include that wheel.
|
||||
existing_pypath = os.environ.get('PYTHONPATH', '')
|
||||
if existing_pypath:
|
||||
return os.pathsep.join((existing_pypath, wheelpath))
|
||||
|
||||
return wheelpath
|
||||
|
||||
|
||||
async def bam_copy(base_blendfile: Path, target_blendfile: Path,
|
||||
@@ -51,6 +50,7 @@ async def bam_copy(base_blendfile: Path, target_blendfile: Path,
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
@@ -66,18 +66,28 @@ async def bam_copy(base_blendfile: Path, target_blendfile: Path,
|
||||
]
|
||||
|
||||
if exclusion_filter:
|
||||
if bam_supports_exclude_option():
|
||||
args.extend(['--exclude', exclusion_filter])
|
||||
else:
|
||||
log.warning('Your version of Blender does not support the exclusion filter, '
|
||||
'copying all files.')
|
||||
args.extend(['--exclude', exclusion_filter])
|
||||
|
||||
cmd_to_log = ' '.join(shlex.quote(s) for s in args)
|
||||
log.info('Executing %s', cmd_to_log)
|
||||
|
||||
# Workaround for Blender 2.78c not having io_blend_utils.pythonpath()
|
||||
if hasattr(io_blend_utils, 'pythonpath'):
|
||||
pythonpath = io_blend_utils.pythonpath()
|
||||
else:
|
||||
pythonpath = wheel_pythonpath_278()
|
||||
|
||||
env = {
|
||||
'PYTHONPATH': pythonpath,
|
||||
# Needed on Windows because http://bugs.python.org/issue8557
|
||||
'PATH': os.environ['PATH'],
|
||||
}
|
||||
if 'SYSTEMROOT' in os.environ: # Windows http://bugs.python.org/issue20614
|
||||
env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*args,
|
||||
env={'PYTHONPATH': io_blend_utils.pythonpath()},
|
||||
env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
|
@@ -11,21 +11,20 @@ class Manager(List, Find):
|
||||
|
||||
@functools.lru_cache()
|
||||
def _sorted_path_replacements(self) -> list:
|
||||
import sys
|
||||
import platform
|
||||
|
||||
if self.path_replacement is None:
|
||||
return []
|
||||
|
||||
print('SORTING PATH REPLACEMENTS')
|
||||
|
||||
items = self.path_replacement.to_dict().items()
|
||||
|
||||
def by_length(item):
|
||||
return -len(item[0]), item[0]
|
||||
|
||||
platform = sys.platform
|
||||
return [(varname, platform_replacements[platform])
|
||||
for varname, platform_replacements in sorted(items, key=by_length)]
|
||||
this_platform = platform.system().lower()
|
||||
return [(varname, platform_replacements[this_platform])
|
||||
for varname, platform_replacements in sorted(items, key=by_length)
|
||||
if this_platform in platform_replacements]
|
||||
|
||||
def replace_path(self, some_path: pathlib.PurePath) -> str:
|
||||
"""Performs path variable replacement.
|
||||
|
@@ -112,7 +112,11 @@ class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
|
||||
async def async_execute(self, context):
|
||||
"""Entry point of the asynchronous operator."""
|
||||
|
||||
self.report({'INFO'}, 'Communicating with Blender Cloud')
|
||||
# We don't want to influence what is included in the screen shot.
|
||||
if self.target == 'SCREENSHOT':
|
||||
print('Blender Cloud add-on is communicating with Blender Cloud')
|
||||
else:
|
||||
self.report({'INFO'}, 'Communicating with Blender Cloud')
|
||||
|
||||
try:
|
||||
# Refresh credentials
|
||||
|
46
blender_cloud/pillar.py
Normal file → Executable file
46
blender_cloud/pillar.py
Normal file → Executable file
@@ -115,6 +115,12 @@ def with_existing_dir(filename: str, open_mode: str, encoding=None):
|
||||
yield file_object
|
||||
|
||||
|
||||
def _shorten(somestr: str, maxlen=40) -> str:
|
||||
"""Shortens strings for logging"""
|
||||
|
||||
return (somestr[:maxlen - 3] + '...') if len(somestr) > maxlen else somestr
|
||||
|
||||
|
||||
def save_as_json(pillar_resource, json_filename):
|
||||
with with_existing_dir(json_filename, 'w') as outfile:
|
||||
log.debug('Saving metadata to %r' % json_filename)
|
||||
@@ -200,8 +206,11 @@ def pillar_api(pillar_endpoint: str = None, caching=True) -> pillarsdk.Api:
|
||||
return _pillar_api[caching]
|
||||
|
||||
|
||||
# No more than this many Pillar calls should be made simultaneously
|
||||
pillar_semaphore = asyncio.Semaphore(3)
|
||||
# This is an asyncio.Semaphore object, which is late-instantiated to be sure
|
||||
# the asyncio loop has been created properly. On Windows we create a new one,
|
||||
# which can cause this semaphore to still be linked against the old default
|
||||
# loop.
|
||||
pillar_semaphore = None
|
||||
|
||||
|
||||
async def pillar_call(pillar_func, *args, caching=True, **kwargs):
|
||||
@@ -214,8 +223,21 @@ async def pillar_call(pillar_func, *args, caching=True, **kwargs):
|
||||
partial = functools.partial(pillar_func, *args, api=pillar_api(caching=caching), **kwargs)
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
async with pillar_semaphore:
|
||||
# Use explicit calls to acquire() and release() so that we have more control over
|
||||
# how long we wait and how we handle timeouts.
|
||||
try:
|
||||
await asyncio.wait_for(pillar_semaphore.acquire(), timeout=10, loop=loop)
|
||||
except asyncio.TimeoutError:
|
||||
log.info('Waiting for semaphore to call %s', pillar_func.__name__)
|
||||
try:
|
||||
await asyncio.wait_for(pillar_semaphore.acquire(), timeout=50, loop=loop)
|
||||
except asyncio.TimeoutError:
|
||||
raise RuntimeError('Timeout waiting for Pillar Semaphore!')
|
||||
|
||||
try:
|
||||
return await loop.run_in_executor(None, partial)
|
||||
finally:
|
||||
pillar_semaphore.release()
|
||||
|
||||
|
||||
def sync_call(pillar_func, *args, caching=True, **kwargs):
|
||||
@@ -441,9 +463,9 @@ async def download_to_file(url, filename, *,
|
||||
log.debug('Downloading was cancelled before doing the GET')
|
||||
raise asyncio.CancelledError('Downloading was cancelled')
|
||||
|
||||
log.debug('Performing GET %s', url)
|
||||
log.debug('Performing GET %s', _shorten(url))
|
||||
response = await loop.run_in_executor(None, perform_get_request)
|
||||
log.debug('Status %i from GET %s', response.status_code, url)
|
||||
log.debug('Status %i from GET %s', response.status_code, _shorten(url))
|
||||
response.raise_for_status()
|
||||
|
||||
if response.status_code == 304:
|
||||
@@ -457,9 +479,9 @@ async def download_to_file(url, filename, *,
|
||||
log.debug('Downloading was cancelled before downloading the GET response')
|
||||
raise asyncio.CancelledError('Downloading was cancelled')
|
||||
|
||||
log.debug('Downloading response of GET %s', url)
|
||||
log.debug('Downloading response of GET %s', _shorten(url))
|
||||
await loop.run_in_executor(None, download_loop)
|
||||
log.debug('Done downloading response of GET %s', url)
|
||||
log.debug('Done downloading response of GET %s', _shorten(url))
|
||||
|
||||
# We're done downloading, now we have something cached we can use.
|
||||
log.debug('Saving header cache to %s', header_store)
|
||||
@@ -534,7 +556,8 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
|
||||
for texture_node in texture_nodes)
|
||||
|
||||
# raises any exception from failed handle_texture_node() calls.
|
||||
await asyncio.gather(*coros)
|
||||
loop = asyncio.get_event_loop()
|
||||
await asyncio.gather(*coros, loop=loop)
|
||||
|
||||
log.info('fetch_texture_thumbs: Done downloading texture thumbnails')
|
||||
|
||||
@@ -747,7 +770,8 @@ async def download_texture(texture_node,
|
||||
future=future)
|
||||
downloaders.append(dlr)
|
||||
|
||||
return await asyncio.gather(*downloaders, return_exceptions=True)
|
||||
loop = asyncio.get_event_loop()
|
||||
return await asyncio.gather(*downloaders, return_exceptions=True, loop=loop)
|
||||
|
||||
|
||||
async def upload_file(project_id: str, file_path: pathlib.Path, *,
|
||||
@@ -773,9 +797,9 @@ async def upload_file(project_id: str, file_path: pathlib.Path, *,
|
||||
log.debug('Uploading was cancelled before doing the POST')
|
||||
raise asyncio.CancelledError('Uploading was cancelled')
|
||||
|
||||
log.debug('Performing POST %s', url)
|
||||
log.debug('Performing POST %s', _shorten(url))
|
||||
response = await loop.run_in_executor(None, upload)
|
||||
log.debug('Status %i from POST %s', response.status_code, url)
|
||||
log.debug('Status %i from POST %s', response.status_code, _shorten(url))
|
||||
response.raise_for_status()
|
||||
|
||||
resp = response.json()
|
||||
|
@@ -457,7 +457,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
|
||||
|
||||
return menu_item
|
||||
|
||||
def update_menu_item(self, node, *args) -> MenuItem:
|
||||
def update_menu_item(self, node, *args):
|
||||
node_uuid = node['_id']
|
||||
|
||||
# Just make this thread-safe to be on the safe side.
|
||||
@@ -538,6 +538,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
|
||||
self.add_menu_item(node, None, 'SPINNER', texture_node['name'])
|
||||
|
||||
def thumbnail_loaded(node, file_desc, thumb_path):
|
||||
self.log.debug('Node %s thumbnail loaded', node['_id'])
|
||||
self.update_menu_item(node, file_desc, thumb_path)
|
||||
|
||||
await pillar.fetch_texture_thumbs(node_uuid, 's', directory,
|
||||
|
@@ -44,6 +44,12 @@ def load_wheel(module_name, fname_prefix):
|
||||
module_name, module.__file__, fname_prefix)
|
||||
return
|
||||
|
||||
sys.path.append(wheel_filename(fname_prefix))
|
||||
module = __import__(module_name)
|
||||
log.debug('Loaded %s from %s', module_name, module.__file__)
|
||||
|
||||
|
||||
def wheel_filename(fname_prefix: str) -> str:
|
||||
path_pattern = os.path.join(my_dir, '%s*.whl' % fname_prefix)
|
||||
wheels = glob.glob(path_pattern)
|
||||
if not wheels:
|
||||
@@ -51,9 +57,7 @@ def load_wheel(module_name, fname_prefix):
|
||||
|
||||
# If there are multiple wheels that match, load the latest one.
|
||||
wheels.sort()
|
||||
sys.path.append(wheels[-1])
|
||||
module = __import__(module_name)
|
||||
log.debug('Loaded %s from %s', module_name, module.__file__)
|
||||
return wheels[-1]
|
||||
|
||||
|
||||
def load_wheels():
|
||||
|
@@ -3,6 +3,7 @@
|
||||
lockfile==0.12.2
|
||||
pillarsdk==1.6.1
|
||||
wheel==0.29.0
|
||||
blender-bam==1.1.7
|
||||
|
||||
# Secondary requirements:
|
||||
cffi==1.6.0
|
||||
|
7
setup.py
7
setup.py
@@ -101,6 +101,11 @@ class BuildWheels(Command):
|
||||
log.info('Downloading Pillar Python SDK wheel')
|
||||
self.download_wheel(requirements['pillarsdk'])
|
||||
|
||||
# Download BAM from pypi. This is required for compatibility with Blender 2.78.
|
||||
if not list(self.wheels_path.glob('blender_bam*.whl')):
|
||||
log.info('Downloading BAM wheel')
|
||||
self.download_wheel(requirements['blender-bam'])
|
||||
|
||||
# Build CacheControl.
|
||||
if not list(self.wheels_path.glob('CacheControl*.whl')):
|
||||
log.info('Building CacheControl in %s', self.cachecontrol_path)
|
||||
@@ -227,7 +232,7 @@ setup(
|
||||
'wheels': BuildWheels},
|
||||
name='blender_cloud',
|
||||
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
|
||||
version='1.7.0',
|
||||
version='1.7.4',
|
||||
author='Sybren A. Stüvel',
|
||||
author_email='sybren@stuvel.eu',
|
||||
packages=find_packages('.'),
|
||||
|
@@ -83,8 +83,13 @@ class PathReplacementTest(unittest.TestCase):
|
||||
|
||||
def _do_test(self, test_paths, platform, pathclass):
|
||||
self.test_manager.PurePlatformPath = pathclass
|
||||
with unittest.mock.patch('sys.platform', platform):
|
||||
|
||||
def mocked_system():
|
||||
return platform
|
||||
|
||||
with unittest.mock.patch('platform.system', mocked_system):
|
||||
for expected_result, input_path in test_paths:
|
||||
as_path_instance = pathclass(input_path)
|
||||
self.assertEqual(expected_result,
|
||||
self.test_manager.replace_path(pathclass(input_path)),
|
||||
'for input %s on platform %s' % (input_path, platform))
|
||||
self.test_manager.replace_path(as_path_instance),
|
||||
'for input %r on platform %s' % (as_path_instance, platform))
|
||||
|
Reference in New Issue
Block a user