Skip to content

Commit

Permalink
Merge pull request tomerfiliba-org#494 from tomerfiliba-org/issue-292
Browse files Browse the repository at this point in the history
Add exposedmethod decorator and documentation replaces PR 292 and resolves 307
  • Loading branch information
comrumino authored Jun 15, 2022
2 parents 5e50641 + 09143fa commit a3b4506
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 5 deletions.
25 changes: 25 additions & 0 deletions docs/docs/services.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,31 @@ that's exposed by the other party. For security concerns, access is only granted
``exposed_`` members. For instance, the ``foo`` method above is inaccessible (attempting to
call it will result in an ``AttributeError``).

Rather than having each method name start with ``exposed_``, you may prefer to use a
decorator. Let's revisit the calculator service, but this time we'll use decorators. ::

import rpyc

@rpyc.service
class CalculatorService(rpyc.Service):
@rpyc.exposed
def add(self, a, b):
return a + b
@rpyc.exposed
def sub(self, a, b):
return a - b
@rpyc.exposed
def mul(self, a, b):
return a * b
@rpyc.exposed
def div(self, a, b):
return a / b
def foo(self):
print "foo"

When implementing services, ``@rpyc.service`` and ``@rpyc.exposed`` can replace the ``exposed_`` naming
convention.

Implementing Services
---------------------
As previously explained, all ``exposed_`` members of your service class will be available to
Expand Down
2 changes: 1 addition & 1 deletion rpyc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
connect_stdpipes, connect, ssl_connect, list_services, discover, connect_by_service, connect_subproc,
connect_thread, ssh_connect)
from rpyc.utils.helpers import async_, timed, buffiter, BgServingThread, restricted
from rpyc.utils import classic
from rpyc.utils import classic, exposed, service
from rpyc.version import version as __version__

from rpyc.lib import setup_logger, spawn
Expand Down
2 changes: 1 addition & 1 deletion rpyc/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# flake8: noqa: F401
from rpyc.core.stream import SocketStream, TunneledSocketStream, PipeStream
from rpyc.core.channel import Channel
from rpyc.core.protocol import Connection
from rpyc.core.protocol import Connection, DEFAULT_CONFIG
from rpyc.core.netref import BaseNetref
from rpyc.core.async_ import AsyncResult, AsyncResultTimeout
from rpyc.core.service import Service, VoidService, SlaveService, MasterService, ClassicService
Expand Down
35 changes: 35 additions & 0 deletions rpyc/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
"""
Utilities (not part of the core protocol)
"""
import functools
import inspect
from rpyc.core import DEFAULT_CONFIG


def service(cls):
"""find and rename exposed decorated attributes"""
for attr_name, attr_obj in inspect.getmembers(cls): # rebind exposed decorated attributes
exposed_prefix = getattr(attr_obj, '__exposed__', False)
if exposed_prefix and not inspect.iscode(attr_obj): # exclude the implementation
renamed = exposed_prefix + attr_name
if inspect.isclass(attr_obj): # recurse exposed objects such as a class
attr_obj = service(attr_obj)
setattr(cls, attr_name, attr_obj)
setattr(cls, renamed, attr_obj)
return cls


def exposed(arg):
"""decorator that adds the exposed prefix information to functions which `service` uses to rebind attrs"""
exposed_prefix = DEFAULT_CONFIG['exposed_prefix']
if isinstance(arg, str):
# When the arg is a string (i.e. `@rpyc.exposed("customPrefix_")`) the prefix
# is partially evaluated into the wrapper. The function returned is "frozen" and used as a decorator.
return functools.partial(_wrapper, arg)
elif hasattr(arg, '__call__'):
# When the arg is callable (i.e. `@rpyc.exposed`) then use default prefix and invoke
return _wrapper(exposed_prefix, arg)
else:
raise TypeError('rpyc.exposed expects a callable object or a string')


def _wrapper(exposed_prefix, exposed_obj):
exposed_obj.__exposed__ = exposed_prefix
return exposed_obj
35 changes: 33 additions & 2 deletions tests/test_custom_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class MyClass(object):
MyClass = MyMeta(MyClass.__name__, MyClass.__bases__, dict(MyClass.__dict__))


@rpyc.service
class MyService(rpyc.Service):
on_connect_called = False
on_disconnect_called = False
Expand Down Expand Up @@ -50,6 +51,24 @@ def exposed_getmeta(self):
def exposed_instance(self, inst, cls):
return isinstance(inst, cls)

@rpyc.exposed
class MyClass(object):
def __init__(self, a, b):
self.a = a
self.b = b

@rpyc.exposed
def foo(self):
return self.a + self.b

@rpyc.exposed
def get_decorated(self):
return "decorated"

@rpyc.exposed('prefix_')
def get_decorated_prefix(self):
return "decorated_prefix"


def before_closed(root):
root.on_about_to_close()
Expand All @@ -61,9 +80,13 @@ class TestCustomService(unittest.TestCase):
def setUp(self):
self.service = MyService()
client_config = {"before_closed": before_closed, "close_catchall": False}
self.conn = rpyc.connect_thread( remote_service=self.service, config=client_config)

prefixed_client_config = {'exposed_prefix': 'prefix_'}
self.conn = rpyc.connect_thread(remote_service=self.service, config=client_config)
self.prefixed_conn = rpyc.connect_thread(remote_service=self.service,
config=prefixed_client_config,
remote_config=prefixed_client_config)
self.conn.root # this will block until the service is initialized,
self.prefixed_conn.root # this will block until the service is initialized,
# so we can be sure on_connect_called is True by that time
self.assertTrue(self.service.on_connect_called)

Expand Down Expand Up @@ -92,6 +115,14 @@ def test_attributes(self):
self.conn.root.exposed_getlist
# this is not an exposed attribute:
self.assertRaises(AttributeError, lambda: self.conn.root.foobar())
# methods exposed using decorator
self.conn.root.get_decorated
self.conn.root.exposed_get_decorated
self.prefixed_conn.root.get_decorated_prefix
self.prefixed_conn.root.prefix_get_decorated_prefix
self.assertFalse(hasattr(self.conn.root, 'get_decorated_prefix'))
smc = self.conn.root.MyClass('a', 'b')
self.assertEquals(smc.foo(), 'ab')

def test_safeattrs(self):
x = self.conn.root.getlist()
Expand Down
12 changes: 11 additions & 1 deletion tests/test_gdb.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import pathlib
import rpyc
import subprocess
import sys
import tempfile
import unittest
import os
from rpyc.utils.server import ThreadedServer
from shutil import which

Expand All @@ -13,9 +15,12 @@ class ParentGDB(rpyc.Service):
def on_connect(self, conn):
tests_path = pathlib.Path(__file__).resolve().parent
gdb_cmd = ['gdb', '-q', '-x', pathlib.Path(tests_path, 'gdb_service.py')]
self._proc = subprocess.Popen(gdb_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
env = os.environ.copy()
env['PYTHONPATH'] = ':'.join(sys.path)
self._proc = subprocess.Popen(gdb_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
stdout = self._proc.stdout.readline()
self._gdb_svc_port = int(stdout.strip().decode())
print(self._gdb_svc_port)
self.gdb_svc_conn = rpyc.connect(host='localhost', port=self._gdb_svc_port)

def on_disconnect(self, conn):
Expand Down Expand Up @@ -50,10 +55,15 @@ def tearDown(self):
pass

def test_gdb(self):
print(0)
parent_gdb_conn = rpyc.connect(host='localhost', port=18878)
print(1)
gdb = parent_gdb_conn.root.get_gdb()
print(2)
gdb.execute('file {}'.format(self.a_out))
print(3)
disasm = gdb.execute('disassemble main', to_string=True)
print(4)
self.assertIn('End of assembler dump', disasm)
parent_gdb_conn.close()

Expand Down

0 comments on commit a3b4506

Please sign in to comment.