Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exposedmethod decorator and documentation replaces PR 292 and resolves 307 #494

Merged
merged 3 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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