Source code for rpy2.robjects.methods

import abc
from types import SimpleNamespace
import typing
from rpy2.robjects.robject import RObjectMixin
import rpy2.rinterface as rinterface
from rpy2.rinterface import StrSexpVector
from rpy2.robjects import help as rhelp
from rpy2.robjects import conversion

_get_exported_value = rinterface.baseenv['::']
getmethod = _get_exported_value('methods', 'getMethod')


require = rinterface.baseenv.find('require')
require(StrSexpVector(('methods', )),
        quietly=rinterface.BoolSexpVector((True, )))


[docs] class RS4(RObjectMixin, rinterface.SexpS4): """ Python representation of an R instance of class 'S4'. """
[docs] def slotnames(self): """ Return the 'slots' defined for this object """ return methods_env['slotNames'](self)
def do_slot(self, name): return (conversion.get_conversion() .rpy2py(super(RS4, self).do_slot(name)))
[docs] def extends(self): """Return the R classes this extends. This calls the R function methods::extends().""" return methods_env['extends'](self.rclass)
[docs] @staticmethod def isclass(name): """ Return whether the given name is a defined class. """ name = conversion.get_conversion().py2rpy(name) return methods_env['isClass'](name)[0]
[docs] def validobject(self, test=False, complete=False): """ Return whether the instance is 'valid' for its class. """ cv = conversion.get_conversion() test = cv.py2rpy(test) complete = cv.py2rpy(complete) return methods_env['validObject'](self, test=test, complete=complete)[0]
class ClassRepresentation(RS4): """Definition of an R S4 class. This can be thought of as the Python "type" (class definition) for an R object with the C-level type S4. """ @property def slots(self): """Slots (attributes) for the class.""" return [y[0] for y in self.do_slot('slots')] @property def basenames(self): """Parent classes.""" return [y[0] for y in self.do_slot('contains')] contains = basenames @property def isabstract(self): """Is the class an abstract class ?""" return self.do_slot('virtual')[0] virtual = isabstract @property def packagename(self): """R package in which the class is defined.""" return self.do_slot('package')[0] package = packagename @property def classname(self): """Name of the R class.""" return self.do_slot('className')[0], def getclassdef(cls_name: str, packagename: typing.Optional[str] = None): package: typing.Union[rinterface._MissingArgType, StrSexpVector] if packagename is None: package = rinterface.MissingArg else: package = StrSexpVector((packagename, )) cls_def = methods_env['getClassDef']( StrSexpVector((cls_name,)), package=package ) if cls_def is rinterface.NULL: raise ValueError( f'{cls_name} is not a class in the R package {packagename!r}' ) cls_def = ClassRepresentation(cls_def) cls_def.__rname__ = cls_name return cls_def class RS4_Type(abc.ABCMeta): def __new__(mcs, name, bases, cls_dict): try: cls_rname = cls_dict['__rname__'] except KeyError: cls_rname = name try: accessors = cls_dict['__accessors__'] except KeyError: accessors = [] for rname, where, \ python_name, as_property, \ docstring in accessors: if where is None: where = rinterface.globalenv else: where = StrSexpVector(('package:%s' % where, )) if python_name is None: python_name = rname signature = StrSexpVector((cls_rname, )) r_meth = getmethod(StrSexpVector((rname, )), signature=signature, where=where) r_meth = conversion.get_conversion().rpy2py(r_meth) if as_property: cls_dict[python_name] = property(r_meth, None, None, doc=docstring) else: cls_dict[python_name] = lambda self: r_meth(self) return type.__new__(mcs, name, bases, cls_dict) # playground to experiment with more metaclass-level automation
[docs] class RS4Auto_Type(abc.ABCMeta): """ This type (metaclass) takes an R S4 class and create a Python class out of it, copying the R documention page into the Python docstring. A class with this metaclass has the following optional attributes: __rname__, __rpackagename__, __attr__translation, __meth_translation__. """ def __new__(mcs, name, bases, cls_dict): try: cls_rname = cls_dict['__rname__'] except KeyError: cls_rname = name try: cls_rpackagename = cls_dict['__rpackagename__'] except KeyError: cls_rpackagename = None try: cls_attr_translation = cls_dict['__attr_translation__'] except KeyError: cls_attr_translation = {} try: cls_meth_translation = cls_dict['__meth_translation__'] except KeyError: cls_meth_translation = {} cls_def = getclassdef(cls_rname, cls_rpackagename) # documentation / help if cls_rpackagename is None: cls_dict['__doc__'] = "Undocumented class from the R workspace." else: pack_help = rhelp.Package(cls_rpackagename) page_help = None try: # R's classes are sometimes documented with a prefix 'class.' page_help = pack_help.fetch('%s-class' % cls_def.__rname__) except rhelp.HelpNotFoundError: pass if page_help is None: try: page_help = pack_help.fetch(cls_def.__rname__) except rhelp.HelpNotFoundError: pass if page_help is None: cls_dict['__doc__'] = ('Unable to fetch R documentation ' 'for the class') else: cls_dict['__doc__'] = ''.join(page_help.to_docstring()) for slt_name in cls_def.slots: # TODO: sanity check on the slot name try: slt_name = cls_attr_translation[slt_name] except KeyError: # no translation: abort pass # TODO: isolate the slot documentation and have it here cls_dict[slt_name] = property(lambda self: self.do_slot(slt_name), None, None, None) # Now tackle the methods all_generics = methods_env['getGenerics']() findmethods = methods_env['findMethods'] # does not seem elegant, but there is probably nothing else to do # than loop across all generics r_cls_rname = StrSexpVector((cls_rname, )) for funcname in all_generics: all_methods = findmethods(StrSexpVector((funcname, )), classes=r_cls_rname) # skip if no methods (issue #301). R's findMethods() result # does not have an attribute "names" if of length zero. if len(all_methods) == 0: continue # all_methods contains all method/signature pairs # having the class we are considering somewhere in the signature # (the R/S4 systems allows multiple dispatch) for name, meth in zip(all_methods.do_slot("names"), all_methods): # R/S4 is storing each method/signature as a string, # with the argument type separated by the character '#' # We will re-use that name for the Python name # (no multiple dispatch in python, the method name # will not be enough), replacing the '#'s with '__'s. signature = name.split("#") meth_name = '__'.join(signature) # function names ending with '<-' indicate that the function # is a setter of some sort. We reflect that by adding a 'set_' # prefix to the Python name (and of course remove the # suffix '<-'). if funcname.endswith('<-'): meth_name = 'set_%s__%s' % (funcname[:-2], meth_name) else: meth_name = '%s__%s' % (funcname, meth_name) # finally replace remaining '.'s in the Python name with '_'s meth_name = meth_name.replace('.', '_') # TODO: sanity check on the function name try: meth_name = cls_meth_translation[meth_name] except KeyError: # no translation: abort pass # TODO: isolate the slot documentation and have it here if meth_name in cls_dict: raise Exception("Duplicated attribute/method name.") cls_dict[meth_name] = meth return abc.ABCMeta.__new__(mcs, name, bases, cls_dict)
def set_accessors(cls, cls_name, where, acs): # set accessors (to be abandonned for the metaclass above ?) if where is None: where = rinterface.globalenv else: where = "package:" + str(where) where = StrSexpVector((where, )) for r_name, python_name, as_property, docstring in acs: if python_name is None: python_name = r_name r_meth = getmethod(StrSexpVector((r_name, )), signature=StrSexpVector((cls_name, )), where=where) r_meth = conversion.get_conversion().rpy2py(r_meth) if as_property: setattr(cls, python_name, property(r_meth, None, None)) else: setattr(cls, python_name, lambda self: r_meth(self)) def get_classnames(packname): res = methods_env['getClasses']( where=StrSexpVector(("package:%s" % packname, )) ) return tuple(res) # Namespace to store the definition of RS4 classes rs4classes = SimpleNamespace() def _getclass(rclsname): if hasattr(rs4classes, rclsname): rcls = getattr(rs4classes, rclsname) else: # dynamically create a class rcls = type(rclsname, (RS4, ), dict()) setattr(rs4classes, rclsname, rcls) return rcls def rs4instance_factory(robj): """ Return an RS4 objects (R objects in the 'S4' class system) as a Python object of type inheriting from `robjects.methods.RS4`. The types are located in the namespace `robjects.methods.rs4classes`, and a dummy type is dynamically created whenever necessary. """ clslist = None if len(robj.rclass) > 1: raise ValueError( 'Currently unable to handle more than one class per object' ) for rclsname in robj.rclass: rcls = _getclass(rclsname) return rcls(robj) if clslist is None: return robj methods_env = rinterface.baseenv.find('as.environment')( StrSexpVector(('package:methods', )) )