# Copyright 2016 Autodesk Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import types
from pyccc import python as bpy
import moldesign as mdt
from moldesign import utils, uibase
from . import configuration
[docs]class RunsRemotely(object):
def __init__(self, enable=True,
display=True,
jobname=None,
sendsource=False,
engine=None,
image=None,
is_imethod=False):
"""Function decorator to run a python function remotely.
Note:
This ONLY works for pure functions - where you're interested in the
return value only. Side effects won't be visible to the user.
Args:
enable (bool): If True, run this job using a compute engine
display (bool): Create a jupyter logging display for the remote job
(default: True in Jupyter notebooks, False otherwise)
jobname (str): Name metadata - defaults to the __name__ of the function
sendsource (bool): if False (default), call this function directly on the remote worker;
if True, send the function's source code (for debugging, mostly)
engine (pyccc.engine.EngineBase): engine to send the job to (default:
moldesign.compute.get_engine())
image (str): name of the docker image (including registry, repository, and tags)
(default: moldesign.config.default_python_image)
is_imethod (bool): This is an instancemethod
Note: we can't determine this at import-time without going to great lengths ...
- see, e.g., http://stackoverflow.com/questions/2366713/ )
"""
self.enabled = enable
self.display = display
self.sendsource = sendsource
self.image = image
self.engine = engine
self.jobname = jobname
self.is_imethod = is_imethod
def __call__(self, func):
"""
This gets called with the function we wish to wrap
"""
assert callable(func)
if self.jobname is None:
self.jobname = func.__name__
if func.__name__ == 'wrapper': assert False
@utils.args_from(func,
wraps=True,
inject_kwargs={'wait': True})
def wrapper(*args, **kwargs):
""" Wraps a python function so that it will be executed remotely using a compute engine
Note:
At runtime, this documentation should be replaced with that of the wrapped function
"""
# If the wrapper is not enabled, just run the wrapped function as normal.
f = func # keeps a reference to the original function in this closure
if not wrapper.enabled:
return f(*args, **kwargs)
wait = kwargs.get('wait', True)
# Bind instance methods to their objects
if self.is_imethod:
f, args = _bind_instance_method(f, args)
# Submit job to remote engine
python_call = bpy.PythonCall(f, *args, **kwargs)
image = utils.if_not_none(self.image, configuration.config.default_python_image)
engine = utils.if_not_none(self.engine, mdt.compute.get_engine())
job = bpy.PythonJob(engine,
image,
python_call,
name=self.jobname,
sendsource=self.sendsource)
if self.display:
uibase.display_log(job.get_display_object(), title=f.__name__)
if wait:
job.wait()
return job.result
else:
return job
wrapper.__name__ = func.__name__
wrapper.enabled = self.enabled
return wrapper
runsremotely = RunsRemotely # because decorators should be lower case
def _bind_instance_method(f, args):
# We can't call this function like normal, because the decorators can't identify
# instance methods. Instead, we'll create another bound copy of the instancemethod (probably
# only need to do this once)
fn_self = args[0]
f = types.MethodType(f, fn_self, fn_self.__class__)
args = args[1:]
return f, args