From f715399f4da08de4a025b7c69066c101310d7b6f Mon Sep 17 00:00:00 2001 From: comrumino Date: Tue, 9 Jan 2024 22:55:16 -0600 Subject: [PATCH] Expanded testing to validate BaseNetref/NetrefMetaclass __mro__ since any a fix for #355 and #547 will be a break-change/major-release. --- rpyc/core/netref.py | 11 ++-- tests/test_netref_hierachy.py | 97 +++++++++++++++++++++++++---------- tests/test_urllib3.py | 42 +++++++++++++++ 3 files changed, 118 insertions(+), 32 deletions(-) create mode 100644 tests/test_urllib3.py diff --git a/rpyc/core/netref.py b/rpyc/core/netref.py index 307a89fe..a5e9a2eb 100644 --- a/rpyc/core/netref.py +++ b/rpyc/core/netref.py @@ -4,7 +4,7 @@ import sys import types from rpyc.lib import get_methods, get_id_pack -from rpyc.lib.compat import pickle, maxint, with_metaclass +from rpyc.lib.compat import pickle, maxint from rpyc.core import consts @@ -83,7 +83,6 @@ class NetrefMetaclass(type): """A *metaclass* used to customize the ``__repr__`` of ``netref`` classes. It is quite useless, but it makes debugging and interactive programming easier""" - __slots__ = () def __repr__(self): @@ -93,7 +92,7 @@ def __repr__(self): return f"" -class BaseNetref(with_metaclass(NetrefMetaclass, object)): +class BaseNetref(object, metaclass=NetrefMetaclass): """The base netref class, from which all netref classes derive. Some netref classes are "pre-generated" and cached upon importing this module (those defined in the :data:`_builtin_types`), and they are shared between all @@ -320,16 +319,18 @@ def class_factory(id_pack, methods): _class = getattr(_module, _class_name, None) if _class is not None and hasattr(_class, '__class__'): class_descriptor = NetrefClass(_class) + elif _class is None: + class_descriptor = NetrefClass(type(_module)) break ns['__class__'] = class_descriptor - netref_name = class_descriptor.owner.__name__ if class_descriptor is not None else name_pack # create methods that must perform a syncreq for name, doc in methods: name = str(name) # IronPython issue #10 # only create methods that won't shadow BaseNetref during merge for mro if name not in LOCAL_ATTRS: # i.e. `name != __class__` ns[name] = _make_method(name, doc) - return type(netref_name, (BaseNetref,), ns) + netref_cls = type(name_pack, (BaseNetref, ), ns) + return netref_cls for _builtin in _builtin_types: diff --git a/tests/test_netref_hierachy.py b/tests/test_netref_hierachy.py index 311758f9..b1cfaac9 100644 --- a/tests/test_netref_hierachy.py +++ b/tests/test_netref_hierachy.py @@ -1,10 +1,15 @@ +import inspect import math import rpyc from rpyc.utils.server import ThreadedServer from rpyc import SlaveService +from rpyc.core import netref import unittest +logger = rpyc.lib.setup_logger() + + class MyMeta(type): def spam(self): @@ -45,11 +50,44 @@ def exposed_instance(self, inst, cls): def exposed_getnonetype(self): """ About the unit test - what's common to types.MethodType and NoneType is that both are - not accessible via builtins. So the unit test I've added in 108ff8e was enough to + not accessible via builtins. So the unit test I've added in 108ff8e was enough to my understanding (implement it with NoneType because that's more easily "created") """ return type(None) +class TestBaseNetrefMRO(unittest.TestCase): + def setUp(self): + self.conn = rpyc.classic.connect_thread() + + def tearDown(self): + self.conn.close() + self.conn = None + + def test_mro(self): + # TODO: netref.class_factory, redesign to register builtin types and better handle generic-aliases/types + # - components to explore: abc.ABCMeta, abc.ABC.register types + # - add mro test for netrefs to remote builtins + self.assertEqual(netref.NetrefMetaclass.__mro__, (netref.NetrefMetaclass, type, object)) + + def test_basenetref(self): + self.assertIsInstance(netref.BaseNetref, netref.NetrefMetaclass) + self.assertIsInstance(netref.BaseNetref, object) + mro = inspect.getmro(netref.BaseNetref) + self.assertEqual(mro, (netref.BaseNetref, object)) + + def test_builtins_dict_netref(self): + cls = netref.builtin_classes_cache['builtins.dict'] + mro_netref = inspect.getmro(cls) + mro_dict = inspect.getmro(dict) + logger.debug('\n') + logger.debug(f'dict_netref: {mro_netref}') + logger.debug(f'dict: {mro_dict}') + self.conn.execute("dict_ = dict(a=0xd35db33f)") + remote_dict = self.conn.namespace['dict_'] + logger.debug(f'remote_dict: {remote_dict}') + self.assertEqual(remote_dict['a'], 3546133311) + + class Test_Netref_Hierarchy(unittest.TestCase): @classmethod @@ -60,7 +98,6 @@ def setUpClass(cls): def setUp(self): self.conn = rpyc.classic.connect('localhost', port=18878) - self.conn2 = None @classmethod def tearDownClass(cls): @@ -68,8 +105,6 @@ def tearDownClass(cls): def tearDown(self): self.conn.close() - if self.conn2 is not None: - self.conn2.close() def test_instancecheck_across_connections(self): self.conn2 = rpyc.classic.connect('localhost', port=18878) @@ -91,9 +126,16 @@ def test_classic(self): isinstance([], x) i = 0 self.assertTrue(type(x).__getitem__(x, i) == x.__getitem__(i)) - _builtins = self.conn.modules.builtins if rpyc.lib.compat.is_py_3k else self.conn.modules.__builtin__ - self.assertEqual(repr(_builtins.float.__class__), repr(type)) - self.assertEqual(repr(type(_builtins.float)), repr(type(_builtins.type))) + + def test_builtins(self): + _builtins = self.conn.modules.builtins + self.assertEqual(repr(_builtins.dict), repr(dict)) # Check repr behavior of netref matchs local + self.assertEqual(repr(type(_builtins.dict.__class__)), repr(type)) # Check netref __class__ is type + self.assertIs(type(_builtins.dict.__class__), type) + # Check class descriptor for netrefs + dict_ = _builtins.dict(space='remote') + self.assertIs(type(dict_).__dict__['__class__'].instance, dict) + self.assertIs(type(dict_).__dict__['__class__'].owner, type) def test_instancecheck_list(self): service = MyService() @@ -122,27 +164,28 @@ def test_StandardError(self): def test_modules(self): """ - >>> type(sys) - # base case - >>> type(conn.modules.sys) - # matches base case - >>> sys.__class__ - # base case - >>> conn.modules.sys.__class__ - # matches base case - >>> type(sys.__class__) - # base case - >>> type(conn.modules.sys.__class__) - # doesn't match. - # ^Should be a netref class of "type" (or maybe just itself?) + >>> type(unittest) + + >>> type(self.conn.modules.unittest) + # reflects that it is a proxy object to unittest + >>> unittest.__class__ + # base case + >>> conn.modules.unittest.__class__ + # matches base case + >>> type(unittest.__class__) + # base case + >>> type(conn.modules.unittest.__class__) + # matches base case """ - import sys - self.assertEqual(repr(sys.__class__), repr(self.conn.modules.sys.__class__)) - # _builtin = sys.modules['builtins' if rpyc.lib.compat.is_py_3k else '__builtins__'].__name__ - # self.assertEqual(repr(type(self.conn.modules.sys)), - # "".format(_builtin)) - # self.assertEqual(repr(type(self.conn.modules.sys.__class__)), - # "".format(_builtin)) + self.assertEqual(repr(self.conn.modules.unittest), repr(unittest)) + self.assertEqual(repr(type(self.conn.modules.unittest)), "") + self.assertIs(self.conn.modules.unittest.__class__, type(unittest)) + self.assertIs(type(self.conn.modules.unittest.__class__), type) + + def test_proxy_instancecheck(self): + self.assertIsInstance(self.conn.modules.builtins.RuntimeError(), Exception) + # TODO: below should pass + # self.assertIsInstance(self.conn.modules.builtins.RuntimeError(), self.conn.modules.builtins.Exception) if __name__ == '__main__': diff --git a/tests/test_urllib3.py b/tests/test_urllib3.py new file mode 100644 index 00000000..15d74560 --- /dev/null +++ b/tests/test_urllib3.py @@ -0,0 +1,42 @@ +import rpyc +import inspect +from rpyc.utils.server import ThreadedServer +from rpyc import SlaveService +import unittest +try: + import urllib3 + _urllib3_import_failed = False +except Except: + _urllib3_import_failed = True + + +@unittest.skipIf(_urllib3_import_failed or True, "urllib3 not available") +class TestUrllib3(unittest.TestCase): + """ #547 """ + + def setUp(self): + self.cfg = {'sync_request_timeout': 60*60} + self.server = ThreadedServer(SlaveService, port=18878, auto_register=False, protocol_config=self.cfg) + self.server.logger.quiet = False + self.server._start_in_thread() + self.conn = rpyc.classic.connect('localhost', port=18878) + + def tearDown(self): + self.conn.close() + self.server.close() + + def test_issue(self): + self.conn.execute('import urllib3') + urllib3_ = self.conn.modules.urllib3 + headers = urllib3.HTTPHeaderDict() + headers.add("Accept", "application/json") + headers.add("Accept", "text/plain") + resp = urllib3_.request( "POST", "https://httpbin.org/post", fields={"hello": "world"}, headers={ "X-Request-Id": "test"}) + __import__('code').interact(local=locals() | globals()) + + + #self.assertTrue(self.conn.root.instance(remote_list, list)) + + +if __name__ == "__main__": + unittest.main()