Compare commits

...

19 Commits

Author SHA1 Message Date
fb5433d473 Bumped version to 1.7.5 2017-10-06 12:39:38 +02:00
a17fe45712 Allow overriding the render output path on a per-scene basis. 2017-10-06 12:39:18 +02:00
1bfba64bdc Formatting 2017-10-06 12:38:17 +02:00
cdb4bf4f4f Renamed 'Job File Path' to 'Job Storage Path' so it's more explicit. 2017-10-06 12:37:44 +02:00
15254b8951 Sorting the project list in user prefs alphabetically 2017-10-06 12:35:25 +02:00
3ed5f2c187 Bumped version to 1.7.4 2017-09-05 11:26:52 +02:00
0be3bf7f49 Fixed unit test, it still mocked sys.platform
We now use platform.system() to detect the platform.
2017-09-05 11:25:25 +02:00
f207e14664 Added link to changelog 2017-09-05 11:16:22 +02:00
9932003400 Fix T48852: screenshot always shows "Communicating with Blender Cloud" 2017-09-05 11:16:17 +02:00
e7035e6f0c Updated changelog 2017-09-05 11:11:31 +02:00
014a36d24e Fix T52621: class name collision upon add-on registration
This is checked since Blender 2.79.
2017-09-05 11:07:33 +02:00
068451a7aa Mark 1.7.3 as released in changelog 2017-09-05 11:06:28 +02:00
56fb1ec3df Bumped version to 1.7.3 2017-08-08 12:46:07 +02:00
e93094cb88 Default to scene frame range when no frame range is given. 2017-07-03 11:09:31 +02:00
33718a1a35 Removed test print statement 2017-07-03 11:09:00 +02:00
db82dbe730 Updated changelog 2017-07-03 09:16:01 +02:00
8d405330ee Better platform detection.
The sys.platform string is 'win32' even on 64-bit Windows. Furthermore,
we expect 'windows', not 'win32'. platform.system().lower() gives us this.
2017-07-03 09:14:27 +02:00
66ddc7b47b Fixed issue running BAM on Windows.
I found this solution in a Django bug report:
 https://code.djangoproject.com/ticket/24160
2017-07-03 09:13:49 +02:00
2fa8cb4054 Refuse to render on Flamenco before blend file is saved at least once.
The file should have a location on the filesystem before BAM can pick it up.
2017-07-03 08:41:26 +02:00
10 changed files with 181 additions and 37 deletions

View File

@@ -1,5 +1,26 @@
# Blender Cloud changelog # Blender Cloud changelog
## Version 1.7.5 (2017-10-06)
- Sorting the project list alphabetically.
- Renamed 'Job File Path' to 'Job Storage Path' so it's more explicit.
- Allow overriding the render output path on a per-scene basis.
## 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) ## Version 1.7.2 (2017-06-22)

View File

@@ -21,7 +21,7 @@
bl_info = { bl_info = {
'name': 'Blender Cloud', 'name': 'Blender Cloud',
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis", "author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 7, 2), 'version': (1, 7, 5),
'blender': (2, 77, 0), 'blender': (2, 77, 0),
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser', '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 ' 'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '

View File

@@ -173,7 +173,7 @@ class AttractPollMixin:
return attract_is_active return attract_is_active
class ToolsPanel(AttractPollMixin, Panel): class AttractToolsPanel(AttractPollMixin, Panel):
bl_label = 'Attract' bl_label = 'Attract'
bl_space_type = 'SEQUENCE_EDITOR' bl_space_type = 'SEQUENCE_EDITOR'
bl_region_type = 'UI' bl_region_type = 'UI'
@@ -974,7 +974,7 @@ def register():
bpy.types.SEQUENCER_PT_edit.append(draw_strip_movie_meta) 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(AttractShotRelink)
bpy.utils.register_class(AttractShotDelete) bpy.utils.register_class(AttractShotDelete)
bpy.utils.register_class(AttractStripUnlink) bpy.utils.register_class(AttractStripUnlink)

View File

@@ -236,7 +236,7 @@ class BlenderCloudPreferences(AddonPreferences):
# NOTE: The assumption is that the workers can also find the files in the same path. # NOTE: The assumption is that the workers can also find the files in the same path.
# This assumption is true for the Blender Institute. # This assumption is true for the Blender Institute.
flamenco_job_file_path = StringProperty( flamenco_job_file_path = StringProperty(
name='Job File Path', name='Job Storage Path',
description='Path where to store job files, should be accesible for Workers too', description='Path where to store job files, should be accesible for Workers too',
subtype='DIR_PATH', subtype='DIR_PATH',
default='/render/_flamenco/storage') default='/render/_flamenco/storage')
@@ -571,7 +571,7 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
pillarsdk.Project.all, pillarsdk.Project.all,
{'where': {'user': self.user_id, {'where': {'user': self.user_id,
'category': {'$ne': 'home'}}, 'category': {'$ne': 'home'}},
'sort': '-_created', 'sort': '-name',
'projection': {'_id': True, 'projection': {'_id': True,
'name': True, 'name': True,
'extension_props': True}, 'extension_props': True},
@@ -581,7 +581,7 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
pillarsdk.Project.all, pillarsdk.Project.all,
{'where': {'user': {'$ne': self.user_id}, {'where': {'user': {'$ne': self.user_id},
'permissions.groups.group': {'$in': self.db_user.groups}}, 'permissions.groups.group': {'$in': self.db_user.groups}},
'sort': '-_created', 'sort': '-name',
'projection': {'_id': True, 'projection': {'_id': True,
'name': True, 'name': True,
'extension_props': True}, 'extension_props': True},
@@ -605,7 +605,10 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
projects = list(reduce_properties(projects_user['_items'])) + \ projects = list(reduce_properties(projects_user['_items'])) + \
list(reduce_properties(projects_shared['_items'])) list(reduce_properties(projects_shared['_items']))
preferences().project.available_projects = projects def proj_sort_key(project):
return project.get('name')
preferences().project.available_projects = sorted(projects, key=proj_sort_key)
self.quit() self.quit()

View File

@@ -20,12 +20,13 @@
The preferences are managed blender.py, the rest of the Flamenco-specific stuff is here. The preferences are managed blender.py, the rest of the Flamenco-specific stuff is here.
""" """
import functools import functools
import logging import logging
import os
from pathlib import Path, PurePath from pathlib import Path, PurePath
import typing import typing
if "bpy" in locals(): if "bpy" in locals():
import importlib import importlib
@@ -37,7 +38,6 @@ if "bpy" in locals():
else: else:
from . import bam_interface, sdk from . import bam_interface, sdk
import bpy import bpy
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty, IntProperty from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty, IntProperty
@@ -45,7 +45,6 @@ from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolPropert
from .. import async_loop, pillar from .. import async_loop, pillar
from ..utils import pyside_cache, redraw from ..utils import pyside_cache, redraw
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Global flag used to determine whether panels etc. can be drawn. # Global flag used to determine whether panels etc. can be drawn.
@@ -150,6 +149,14 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
log = logging.getLogger('%s.FLAMENCO_OT_render' % __name__) log = logging.getLogger('%s.FLAMENCO_OT_render' % __name__)
async def async_execute(self, context): 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): if not await self.authenticate(context):
return return
@@ -192,10 +199,12 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
# Create the job at Flamenco Server. # Create the job at Flamenco Server.
context.window_manager.flamenco_status = 'COMMUNICATING' context.window_manager.flamenco_status = 'COMMUNICATING'
frame_range = scene.flamenco_render_frame_range.strip() or scene_frame_range(context)
settings = {'blender_cmd': '{blender}', settings = {'blender_cmd': '{blender}',
'chunk_size': scene.flamenco_render_fchunk_size, 'chunk_size': scene.flamenco_render_fchunk_size,
'filepath': manager.replace_path(outfile), 'filepath': manager.replace_path(outfile),
'frames': scene.flamenco_render_frame_range, 'frames': frame_range,
'render_output': manager.replace_path(render_output), 'render_output': manager.replace_path(render_output),
} }
@@ -348,6 +357,13 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
return outfile, missing_sources 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): class FLAMENCO_OT_scene_to_frame_range(FlamencoPollMixin, Operator):
"""Sets the scene frame range as the Flamenco render frame range.""" """Sets the scene frame range as the Flamenco render frame range."""
bl_idname = 'flamenco.scene_to_frame_range' bl_idname = 'flamenco.scene_to_frame_range'
@@ -355,8 +371,7 @@ class FLAMENCO_OT_scene_to_frame_range(FlamencoPollMixin, Operator):
bl_description = __doc__.rstrip('.') bl_description = __doc__.rstrip('.')
def execute(self, context): def execute(self, context):
s = context.scene context.scene.flamenco_render_frame_range = scene_frame_range(context)
s.flamenco_render_frame_range = '%i-%i' % (s.frame_start, s.frame_end)
return {'FINISHED'} return {'FINISHED'}
@@ -437,6 +452,30 @@ class FLAMENCO_OT_explore_file_path(FlamencoPollMixin,
return {'FINISHED'} return {'FINISHED'}
class FLAMENCO_OT_enable_output_path_override(Operator):
"""Enables the 'override output path' setting."""
bl_idname = 'flamenco.enable_output_path_override'
bl_label = 'Enable Overriding of Output Path'
bl_description = 'Click to specify a non-default Output Path for this particular job'
def execute(self, context):
context.scene.flamenco_do_override_output_path = True
return {'FINISHED'}
class FLAMENCO_OT_disable_output_path_override(Operator):
"""Disables the 'override output path' setting."""
bl_idname = 'flamenco.disable_output_path_override'
bl_label = 'disable Overriding of Output Path'
bl_description = 'Click to use the default Output Path'
def execute(self, context):
context.scene.flamenco_do_override_output_path = False
return {'FINISHED'}
async def create_job(user_id: str, async def create_job(user_id: str,
project_id: str, project_id: str,
manager_id: str, manager_id: str,
@@ -491,6 +530,7 @@ def _render_output_path(
flamenco_job_output_path: str, flamenco_job_output_path: str,
render_image_format: str, render_image_format: str,
flamenco_render_frame_range: str, flamenco_render_frame_range: str,
include_rel_path: bool = True,
) -> typing.Optional[PurePath]: ) -> typing.Optional[PurePath]:
"""Cached version of render_output_path() """Cached version of render_output_path()
@@ -515,8 +555,7 @@ def _render_output_path(
except ValueError: except ValueError:
return None return None
rel_parts = proj_rel.parts[flamenco_job_output_strip_components:] output_top = PurePath(flamenco_job_output_path)
output_top = Path(flamenco_job_output_path)
# Strip off '.flamenco' too; we use 'xxx.flamenco.blend' as job file, but # Strip off '.flamenco' too; we use 'xxx.flamenco.blend' as job file, but
# don't want to have all the output paths ending in '.flamenco'. # don't want to have all the output paths ending in '.flamenco'.
@@ -524,7 +563,11 @@ def _render_output_path(
if stem.endswith('.flamenco'): if stem.endswith('.flamenco'):
stem = stem[:-9] stem = stem[:-9]
dir_components = output_top.joinpath(*rel_parts) / stem if include_rel_path:
rel_parts = proj_rel.parts[flamenco_job_output_strip_components:]
dir_components = output_top.joinpath(*rel_parts) / stem
else:
dir_components = output_top
# Blender will have to append the file extensions by itself. # Blender will have to append the file extensions by itself.
if is_image_type(render_image_format): if is_image_type(render_image_format):
@@ -549,13 +592,19 @@ def render_output_path(context, filepath: Path = None) -> typing.Optional[PurePa
if filepath is None: if filepath is None:
filepath = Path(context.blend_data.filepath) filepath = Path(context.blend_data.filepath)
if scene.flamenco_do_override_output_path:
job_output_path = scene.flamenco_override_output_path
else:
job_output_path = prefs.flamenco_job_output_path
return _render_output_path( return _render_output_path(
prefs.cloud_project_local_path, prefs.cloud_project_local_path,
filepath, filepath,
prefs.flamenco_job_output_strip_components, prefs.flamenco_job_output_strip_components,
prefs.flamenco_job_output_path, job_output_path,
scene.render.image_settings.file_format, scene.render.image_settings.file_format,
scene.flamenco_render_frame_range, scene.flamenco_render_frame_range,
include_rel_path=not scene.flamenco_do_override_output_path,
) )
@@ -589,8 +638,9 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
if getattr(context.scene, 'flamenco_render_job_type', None) == 'blender-render-progressive': if getattr(context.scene, 'flamenco_render_job_type', None) == 'blender-render-progressive':
layout.prop(context.scene, 'flamenco_render_schunk_count') layout.prop(context.scene, 'flamenco_render_schunk_count')
readonly_stuff = layout.column(align=True) paths_layout = layout.column(align=True)
labeled_row = readonly_stuff.split(0.25, align=True)
labeled_row = paths_layout.split(0.25, align=True)
labeled_row.label('Storage:') labeled_row.label('Storage:')
prop_btn_row = labeled_row.row(align=True) prop_btn_row = labeled_row.row(align=True)
prop_btn_row.label(prefs.flamenco_job_file_path) prop_btn_row.label(prefs.flamenco_job_file_path)
@@ -598,19 +648,33 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
text='', icon='DISK_DRIVE') text='', icon='DISK_DRIVE')
props.path = prefs.flamenco_job_file_path props.path = prefs.flamenco_job_file_path
labeled_row = readonly_stuff.split(0.25, align=True)
labeled_row.label('Output:')
prop_btn_row = labeled_row.row(align=True)
render_output = render_output_path(context) render_output = render_output_path(context)
if render_output is None: if render_output is None:
prop_btn_row.label('Unable to render with Flamenco, outside of project directory.') paths_layout.label('Unable to render with Flamenco, outside of project directory.')
else: else:
prop_btn_row.label(str(render_output)) labeled_row = paths_layout.split(0.25, align=True)
labeled_row.label('Output:')
prop_btn_row = labeled_row.row(align=True)
if context.scene.flamenco_do_override_output_path:
prop_btn_row.prop(context.scene, 'flamenco_override_output_path', text='')
op = FLAMENCO_OT_disable_output_path_override.bl_idname
icon = 'X'
else:
prop_btn_row.label(str(render_output))
op = FLAMENCO_OT_enable_output_path_override.bl_idname
icon = 'GREASEPENCIL'
prop_btn_row.operator(op, icon=icon, text='')
props = prop_btn_row.operator(FLAMENCO_OT_explore_file_path.bl_idname, props = prop_btn_row.operator(FLAMENCO_OT_explore_file_path.bl_idname,
text='', icon='DISK_DRIVE') text='', icon='DISK_DRIVE')
props.path = str(render_output.parent) props.path = str(render_output.parent)
if context.scene.flamenco_do_override_output_path:
labeled_row = paths_layout.split(0.25, align=True)
labeled_row.label('Effective Output Path:')
labeled_row.label(str(render_output))
flamenco_status = context.window_manager.flamenco_status flamenco_status = context.window_manager.flamenco_status
if flamenco_status == 'IDLE': if flamenco_status == 'IDLE':
layout.operator(FLAMENCO_OT_render.bl_idname, layout.operator(FLAMENCO_OT_render.bl_idname,
@@ -642,6 +706,22 @@ def deactivate():
_render_output_path.cache_clear() _render_output_path.cache_clear()
def flamenco_do_override_output_path_updated(scene, context):
"""Set the override paths to the default, if not yet set."""
# Only set a default when enabling the override.
if not scene.flamenco_do_override_output_path:
return
# Don't overwrite existing setting.
if scene.flamenco_override_output_path:
return
from ..blender import preferences
scene.flamenco_override_output_path = preferences().flamenco_job_output_path
log.info('Setting Override Output Path to %s', scene.flamenco_override_output_path)
def register(): def register():
from ..utils import redraw from ..utils import redraw
@@ -651,6 +731,8 @@ def register():
bpy.utils.register_class(FLAMENCO_OT_scene_to_frame_range) bpy.utils.register_class(FLAMENCO_OT_scene_to_frame_range)
bpy.utils.register_class(FLAMENCO_OT_copy_files) bpy.utils.register_class(FLAMENCO_OT_copy_files)
bpy.utils.register_class(FLAMENCO_OT_explore_file_path) bpy.utils.register_class(FLAMENCO_OT_explore_file_path)
bpy.utils.register_class(FLAMENCO_OT_enable_output_path_override)
bpy.utils.register_class(FLAMENCO_OT_disable_output_path_override)
bpy.utils.register_class(FLAMENCO_PT_render) bpy.utils.register_class(FLAMENCO_PT_render)
scene = bpy.types.Scene scene = bpy.types.Scene
@@ -688,6 +770,19 @@ def register():
description='Higher numbers mean higher priority' description='Higher numbers mean higher priority'
) )
scene.flamenco_do_override_output_path = BoolProperty(
name='Override Output Path for this Job',
description='When enabled, allows you to specify a non-default Output path '
'for this particular job',
default=False,
update=flamenco_do_override_output_path_updated
)
scene.flamenco_override_output_path = StringProperty(
name='Override Output Path',
description='Path where to store output files, should be accessible for Workers',
subtype='DIR_PATH',
default='')
bpy.types.WindowManager.flamenco_status = EnumProperty( bpy.types.WindowManager.flamenco_status = EnumProperty(
items=[ items=[
('IDLE', 'IDLE', 'Not doing anything.'), ('IDLE', 'IDLE', 'Not doing anything.'),
@@ -724,6 +819,14 @@ def unregister():
del bpy.types.Scene.flamenco_render_job_priority del bpy.types.Scene.flamenco_render_job_priority
except AttributeError: except AttributeError:
pass pass
try:
del bpy.types.Scene.flamenco_do_override_output_path
except AttributeError:
pass
try:
del bpy.types.Scene.flamenco_override_output_path
except AttributeError:
pass
try: try:
del bpy.types.WindowManager.flamenco_status del bpy.types.WindowManager.flamenco_status
except AttributeError: except AttributeError:

View File

@@ -50,6 +50,7 @@ async def bam_copy(base_blendfile: Path, target_blendfile: Path,
""" """
import asyncio import asyncio
import os
import shlex import shlex
import subprocess import subprocess
@@ -76,9 +77,17 @@ async def bam_copy(base_blendfile: Path, target_blendfile: Path,
else: else:
pythonpath = wheel_pythonpath_278() 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( proc = await asyncio.create_subprocess_exec(
*args, *args,
env={'PYTHONPATH': pythonpath}, env=env,
stdin=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,

View File

@@ -11,21 +11,20 @@ class Manager(List, Find):
@functools.lru_cache() @functools.lru_cache()
def _sorted_path_replacements(self) -> list: def _sorted_path_replacements(self) -> list:
import sys import platform
if self.path_replacement is None: if self.path_replacement is None:
return [] return []
print('SORTING PATH REPLACEMENTS')
items = self.path_replacement.to_dict().items() items = self.path_replacement.to_dict().items()
def by_length(item): def by_length(item):
return -len(item[0]), item[0] return -len(item[0]), item[0]
platform = sys.platform this_platform = platform.system().lower()
return [(varname, platform_replacements[platform]) return [(varname, platform_replacements[this_platform])
for varname, platform_replacements in sorted(items, key=by_length)] 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: def replace_path(self, some_path: pathlib.PurePath) -> str:
"""Performs path variable replacement. """Performs path variable replacement.

View File

@@ -112,7 +112,11 @@ class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
async def async_execute(self, context): async def async_execute(self, context):
"""Entry point of the asynchronous operator.""" """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: try:
# Refresh credentials # Refresh credentials

View File

@@ -232,7 +232,7 @@ setup(
'wheels': BuildWheels}, 'wheels': BuildWheels},
name='blender_cloud', name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.', description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.7.2', version='1.7.5',
author='Sybren A. Stüvel', author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu', author_email='sybren@stuvel.eu',
packages=find_packages('.'), packages=find_packages('.'),

View File

@@ -83,8 +83,13 @@ class PathReplacementTest(unittest.TestCase):
def _do_test(self, test_paths, platform, pathclass): def _do_test(self, test_paths, platform, pathclass):
self.test_manager.PurePlatformPath = 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: for expected_result, input_path in test_paths:
as_path_instance = pathclass(input_path)
self.assertEqual(expected_result, self.assertEqual(expected_result,
self.test_manager.replace_path(pathclass(input_path)), self.test_manager.replace_path(as_path_instance),
'for input %s on platform %s' % (input_path, platform)) 'for input %r on platform %s' % (as_path_instance, platform))