Skip to content

Commit

Permalink
Removed sys.path.append in RPC module (#70)
Browse files Browse the repository at this point in the history
* updated rpc imports to avoid relative import when rpc module is not on sys path

* made base_server import relative

* updated groom extension and extension docs

* fix unreal default import
  • Loading branch information
jack-yao91 authored Aug 8, 2024
1 parent 272290a commit 0fad31f
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 40 deletions.
10 changes: 4 additions & 6 deletions docs/send2ue/customize/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,9 @@ There is a submodule within `send2ue` that can be used to make your own rpc call
a basic example of how you can force an asset to be renamed in the `post_import` method of an extension.
```python
from send2ue.core.extension import ExtensionBase
from send2ue.dependencies.unreal import remote_unreal_decorator
from send2ue.dependencies.rpc.factory import make_remote


@remote_unreal_decorator
def rename_unreal_asset(source_asset_path, destination_asset_path):
if unreal.EditorAssetLibrary.does_asset_exist(destination_asset_path):
unreal.EditorAssetLibrary.delete_asset(destination_asset_path)
Expand All @@ -343,11 +342,10 @@ class ExampleExtension(ExtensionBase):
name = 'example'
def post_import(self):
asset_path = self.asset_data[self.asset_id]['asset_path']
rename_unreal_asset(asset_path, f'{asset_path}_renamed_again')
remote_rename_unreal_asset = make_remote(rename_unreal_asset)
remote_rename_unreal_asset(asset_path, f'{asset_path}_renamed_again')
```
Notice how you can define remote unreal functions on the fly by just wrapping your function
with the `remote_unreal_decorator`. The RPC library has a factory that takes care of teleporting
your code and imports over to the open unreal editor.
Notice how you can define remote unreal functions on the fly by just passing a function reference to the `make_remote` function. The RPC library has a factory that takes care of teleporting your code and imports over to the open unreal editor.

!!! note

Expand Down
6 changes: 5 additions & 1 deletion src/addons/send2ue/core/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import os
import bpy
from ..dependencies.unreal import UnrealRemoteCalls, is_connected
from ..dependencies.unreal import is_connected
from ..dependencies.unreal import UnrealRemoteCalls as UnrealCalls
from ..dependencies.rpc.factory import make_remote

UnrealRemoteCalls = make_remote(UnrealCalls)


def set_property_error_message(property_name, error_message):
Expand Down
5 changes: 4 additions & 1 deletion src/addons/send2ue/core/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import bpy
from . import settings, extension
from ..constants import PathModes, ExtensionTasks, UnrealTypes
from ..dependencies.unreal import UnrealRemoteCalls
from ..dependencies.unreal import UnrealRemoteCalls as UnrealCalls
from .utilities import track_progress, get_asset_id
from ..dependencies.rpc.factory import make_remote

UnrealRemoteCalls = make_remote(UnrealCalls)


@track_progress(message='Importing asset "{attribute}"...', attribute='file_path')
Expand Down
5 changes: 4 additions & 1 deletion src/addons/send2ue/core/validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import os
import bpy
from . import utilities, formatting, extension
from ..dependencies.unreal import UnrealRemoteCalls
from ..constants import BlenderTypes, PathModes, ToolInfo, Extensions, ExtensionTasks, RegexPresets
from ..dependencies.unreal import UnrealRemoteCalls as UnrealCalls
from ..dependencies.rpc.factory import make_remote

UnrealRemoteCalls = make_remote(UnrealCalls)


class ValidationManager:
Expand Down
105 changes: 105 additions & 0 deletions src/addons/send2ue/dependencies/rpc/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import inspect
import textwrap
import unittest
from typing import Any, Iterator, Optional, Union, List, Tuple, Callable
from xmlrpc.client import Fault

from .client import RPCClient
Expand Down Expand Up @@ -265,6 +266,110 @@ def decorate(cls):
return cls
return decorate

def execute_remotely(
port: int,
function: Callable,
args: Optional[Union[List, Tuple]] = None,
kwargs: Optional[dict] = None,
remap_pairs: Optional[List[Tuple[str, str]]] = None,
default_imports: Optional[List[str]] = None,
):
"""
Executes the given function remotely.
"""
if not args:
args = []

validate_file_is_saved(function)
validate_key_word_parameters(function, kwargs)
rpc_factory = RPCFactory(
rpc_client=RPCClient(port),
remap_pairs=remap_pairs,
default_imports=default_imports
)
return rpc_factory.run_function_remotely(function, args)

def _make_remote(
port: int,
remap_pairs: Optional[List[Tuple[str, str]]] = None,
default_imports: Optional[List[str]] = None
):
def decorator(function):
def wrapper(*args, **kwargs):
validate_key_word_parameters(function, kwargs)
return execute_remotely(
function=function,
args=args,
kwargs=kwargs,
port=port,
remap_pairs=remap_pairs,
default_imports=default_imports
)

return wrapper

return decorator


def get_all_parent_classes(cls) -> Iterator[Any]:
"""
Gets all parent classes recursively upward from the given class.
"""
for _cls in cls.__bases__:
if object not in _cls.__bases__ and len(_cls.__bases__) >= 1:
yield from get_all_parent_classes(_cls)
yield _cls

def make_remote(
reference: Any,
port: Optional[int] = None,
default_imports: Optional[List[str]] = None,
) -> Callable:
"""
Makes the given class or function run remotely when invoked.
"""
if not default_imports:
default_imports = ['import unreal']
remap_pairs = []
unreal_port = int(os.environ.get('UNREAL_PORT', 9998))

# use a different remap pairs when inside a container
if os.environ.get('TEST_ENVIRONMENT'):
unreal_port = int(os.environ.get('UNREAL_PORT', 8998))
remap_pairs = [(os.environ.get('HOST_REPO_FOLDER', ''), os.environ.get('CONTAINER_REPO_FOLDER', ''))]

if not port:
port = unreal_port

# if this is not a class then decorate it
if not inspect.isclass(reference):
return _make_remote(
port=port,
remap_pairs=remap_pairs,
default_imports=default_imports
)(reference)

# if this is a class then decorate all its methods
methods = {}
for _cls in [*get_all_parent_classes(reference), reference]:
for attribute, value in _cls.__dict__.items():
# dont look at magic methods
if not attribute.startswith('__'):
validate_class_method(_cls, value)
# note that we use getattr instead of passing the value directly.
methods[attribute] = _make_remote(
port=port,
remap_pairs=remap_pairs,
default_imports=default_imports
)(getattr(_cls, attribute))

# return a new class with the decorated methods all in the same class
return type(
reference.__name__,
(object,),
methods
)


class RPCTestCase(unittest.TestCase):
"""
Expand Down
9 changes: 1 addition & 8 deletions src/addons/send2ue/dependencies/rpc/server.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import os
import sys
sys.path.append(os.path.dirname(__file__))

from base_server import BaseRPCServerManager
from .base_server import BaseRPCServerManager


class RPCServer(BaseRPCServerManager):
Expand All @@ -13,8 +11,3 @@ def __init__(self):
super(RPCServer, self).__init__()
self.name = 'RPCServer'
self.port = int(os.environ.get('RPC_PORT', 9998))


if __name__ == '__main__':
rpc_server = RPCServer()
rpc_server.start(threaded=False)
23 changes: 7 additions & 16 deletions src/addons/send2ue/dependencies/unreal.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,17 @@
from xmlrpc.client import ProtocolError
from http.client import RemoteDisconnected

sys.path.append(os.path.dirname(__file__))
import rpc.factory
import remote_execution

try:
import unreal
except ModuleNotFoundError:
pass

REMAP_PAIRS = []
UNREAL_PORT = int(os.environ.get('UNREAL_PORT', 9998))

# use a different remap pairs when inside a container
if os.environ.get('TEST_ENVIRONMENT'):
UNREAL_PORT = int(os.environ.get('UNREAL_PORT', 8998))
REMAP_PAIRS = [(os.environ.get('HOST_REPO_FOLDER'), os.environ.get('CONTAINER_REPO_FOLDER'))]

# this defines a the decorator that makes function run as remote call in unreal
remote_unreal_decorator = rpc.factory.remote_call(
port=UNREAL_PORT,
default_imports=['import unreal'],
remap_pairs=REMAP_PAIRS,
)

unreal_response = ''


Expand Down Expand Up @@ -152,6 +140,8 @@ def run_commands(commands):
:param list commands: A formatted string of python commands that will be run by unreal engine.
:return str: The stdout produced by the remote python command.
"""
from . import remote_execution

# wrap the commands in a try except so that all exceptions can be logged in the output
commands = ['try:'] + add_indent(commands, '\t') + ['except Exception as error:', '\tprint(error)']

Expand All @@ -168,7 +158,8 @@ def is_connected():
Checks the rpc server connection
"""
try:
rpc_client = rpc.client.RPCClient(port=UNREAL_PORT)
from .rpc import client
rpc_client = client.RPCClient(port=UNREAL_PORT)
return rpc_client.proxy.is_running()
except (RemoteDisconnected, ConnectionRefusedError, ProtocolError):
return False
Expand All @@ -178,7 +169,8 @@ def set_rpc_env(key, value):
"""
Sets an env value on the unreal RPC server.
"""
rpc_client = rpc.client.RPCClient(port=UNREAL_PORT)
from .rpc import client
rpc_client = client.RPCClient(port=UNREAL_PORT)
rpc_client.proxy.set_env(key, value)


Expand Down Expand Up @@ -986,7 +978,6 @@ def run_import(self, import_type='control_rig'):
)


@rpc.factory.remote_class(remote_unreal_decorator)
class UnrealRemoteCalls:
@staticmethod
def get_lod_count(asset_path):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from send2ue.core.extension import ExtensionBase
from send2ue.core import utilities
from send2ue.constants import UnrealTypes
from send2ue.dependencies.unreal import UnrealRemoteCalls
from send2ue.dependencies.unreal import UnrealRemoteCalls as UnrealCalls
from send2ue.dependencies.rpc.factory import make_remote


class CreatePostImportAssetsForGroom(ExtensionBase):
Expand Down Expand Up @@ -56,6 +57,7 @@ def post_import(self, asset_data, properties):
mesh_asset_data = utilities.get_related_mesh_asset_data_from_groom_asset_data(asset_data)
groom_asset_path = asset_data.get('asset_path', '')
mesh_asset_path = mesh_asset_data.get('asset_path', '')
UnrealRemoteCalls = make_remote(UnrealCalls)

if not UnrealRemoteCalls.asset_exists(groom_asset_path):
return
Expand Down
5 changes: 3 additions & 2 deletions src/addons/send2ue/resources/extensions/instance_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import os
from send2ue.constants import UnrealTypes
from send2ue.core.extension import ExtensionBase
from send2ue.dependencies.unreal import UnrealRemoteCalls
from send2ue.core.utilities import (
convert_blender_rotation_to_unreal_rotation,
convert_blender_to_unreal_location,
get_armature_modifier_rig_object,
get_asset_name
)
from send2ue.dependencies.unreal import UnrealRemoteCalls as UnrealCalls
from send2ue.dependencies.rpc.factory import make_remote

STATIC_MESH_INSTANCE_NAMES = []
SKELETAL_MESH_INSTANCE_NAMES = []
Expand Down Expand Up @@ -162,7 +163,7 @@ def post_import(self, asset_data, properties):
break

if unique_name:
UnrealRemoteCalls.instance_asset(
make_remote(UnrealCalls).instance_asset(
asset_data['asset_path'],
convert_blender_to_unreal_location(location),
convert_blender_rotation_to_unreal_rotation(rotation),
Expand Down
6 changes: 3 additions & 3 deletions tests/test_files/send2ue_extensions/example_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
import bpy
from pprint import pprint
from send2ue.core.extension import ExtensionBase
from send2ue.dependencies.unreal import remote_unreal_decorator
from send2ue.dependencies.rpc.factory import make_remote


@remote_unreal_decorator
def rename_unreal_asset(source_asset_path, destination_asset_path):
if unreal.EditorAssetLibrary.does_asset_exist(destination_asset_path):
unreal.EditorAssetLibrary.delete_asset(destination_asset_path)
Expand Down Expand Up @@ -99,7 +98,8 @@ def post_import(self, asset_data, properties):
print('After the import task')
asset_path = asset_data.get('asset_path')
if asset_path:
rename_unreal_asset(asset_path, f'{asset_path}_renamed_again')
remote_rename_unreal_asset = make_remote(rename_unreal_asset)
remote_rename_unreal_asset(asset_path, f'{asset_path}_renamed_again')

def post_operation(self, properties):
"""
Expand Down
3 changes: 2 additions & 1 deletion tests/utils/base_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ def __init__(self, *args, **kwargs):

from utils.blender import BlenderRemoteCalls
from send2ue.dependencies.unreal import UnrealRemoteCalls
from send2ue.dependencies.rpc.factory import make_remote
self.blender = BlenderRemoteCalls
self.unreal = UnrealRemoteCalls
self.unreal = make_remote(UnrealRemoteCalls)

def setUp(self):
# load in the object from the file you will run tests with
Expand Down

0 comments on commit 0fad31f

Please sign in to comment.