1import os 2import typing 3import warnings 4from types import ModuleType 5from warnings import warn 6import rpy2.rinterface as rinterface 7from . import conversion 8from rpy2.robjects.functions import (SignatureTranslatedFunction, 9 docstring_property, 10 DocumentedSTFunction) 11from rpy2.robjects import Environment 12from rpy2.robjects.packages_utils import ( 13 default_symbol_r2python, 14 default_symbol_resolve, 15 _map_symbols, 16 _fix_map_symbols 17) 18import rpy2.robjects.help as rhelp 19 20_require = rinterface.baseenv['require'] 21_library = rinterface.baseenv['library'] 22_as_env = rinterface.baseenv['as.environment'] 23_package_has_namespace = rinterface.baseenv['packageHasNamespace'] 24_system_file = rinterface.baseenv['system.file'] 25_get_namespace = rinterface.baseenv['getNamespace'] 26_get_namespace_version = rinterface.baseenv['getNamespaceVersion'] 27_get_namespace_exports = rinterface.baseenv['getNamespaceExports'] 28_loaded_namespaces = rinterface.baseenv['loadedNamespaces'] 29_globalenv = rinterface.globalenv 30_new_env = rinterface.baseenv["new.env"] 31 32StrSexpVector = rinterface.StrSexpVector 33# Fetching symbols in the namespace "utils" assumes that "utils" is loaded 34# (currently the case by default in R). 35_data = rinterface.baseenv['::'](StrSexpVector(('utils', )), 36 StrSexpVector(('data', ))) 37 38_reval = rinterface.baseenv['eval'] 39_options = rinterface.baseenv['options'] 40 41 42def no_warnings(func): 43 """ Decorator to run R functions without warning. """ 44 def run_withoutwarnings(*args, **kwargs): 45 warn_i = _options().do_slot('names').index('warn') 46 oldwarn = _options()[warn_i][0] 47 _options(warn=-1) 48 try: 49 res = func(*args, **kwargs) 50 except Exception as e: 51 # restore the old warn setting before propagating 52 # the exception up 53 _options(warn=oldwarn) 54 raise e 55 _options(warn=oldwarn) 56 return res 57 return run_withoutwarnings 58 59 60@no_warnings 61def _eval_quiet(expr): 62 return _reval(expr) 63 64 65# FIXME: should this be part of the API for rinterface ? 66# (may be it is already the case and there is code 67# duplicaton ?) 68def reval(string: str, 69 envir: typing.Optional[rinterface.SexpEnvironment] = None): 70 """ Evaluate a string as R code 71 :param string: R code 72 :type string: a :class:`str` 73 :param envir: Optional environment to evaluate the R code. 74 """ 75 p = rinterface.parse(string) 76 res = _reval(p, envir=envir) 77 return res 78 79 80def quiet_require(name: str, lib_loc=None): 81 """ Load an R package /quietly/ (suppressing messages to the console). """ 82 if lib_loc is None: 83 lib_loc = "NULL" 84 else: 85 lib_loc = "\"%s\"" % (lib_loc.replace('"', '\\"')) 86 expr_txt = ("suppressPackageStartupMessages(" 87 "base::require(%s, lib.loc=%s))" 88 % (name, lib_loc)) 89 expr = rinterface.parse(expr_txt) 90 ok = _eval_quiet(expr) 91 return ok 92 93 94class PackageData(object): 95 """ Datasets in an R package. 96 In R datasets can be distributed with a package. 97 98 Datasets can be: 99 100 - serialized R objects 101 102 - R code (that produces the dataset) 103 104 For a given R packages, datasets are stored separately from the rest 105 of the code and are evaluated/loaded lazily. 106 107 The lazy aspect has been conserved and the dataset are only loaded 108 or generated when called through the method 'fetch()'. 109 """ 110 _packagename = None 111 _lib_loc = None 112 _datasets = None 113 114 def __init__(self, packagename, lib_loc=rinterface.NULL): 115 self._packagename = packagename 116 self._lib_loc 117 118 def _init_setlist(self): 119 _datasets = dict() 120 # 2D array of information about datatsets 121 tmp_m = _data(**{'package': StrSexpVector((self._packagename, )), 122 'lib.loc': self._lib_loc})[2] 123 nrows, ncols = tmp_m.do_slot('dim') 124 c_i = 2 125 for r_i in range(nrows): 126 _datasets[tmp_m[r_i + c_i * nrows]] = None 127 # FIXME: check if instance methods are overriden 128 self._datasets = _datasets 129 130 def names(self): 131 """ Names of the datasets""" 132 if self._datasets is None: 133 self._init_setlist() 134 return self._datasets.keys() 135 136 def fetch(self, name): 137 """ Fetch the dataset (loads it or evaluates the R associated 138 with it. 139 140 In R, datasets are loaded into the global environment by default 141 but this function returns an environment that contains the dataset(s). 142 """ 143 if self._datasets is None: 144 self._init_setlist() 145 146 if name not in self._datasets: 147 raise KeyError('Data set "%s" cannot be found' % name) 148 env = _new_env() 149 _data(StrSexpVector((name, )), 150 **{'package': StrSexpVector((self._packagename, )), 151 'lib.loc': self._lib_loc, 152 'envir': env}) 153 return Environment(env) 154 155 156class Package(ModuleType): 157 """ Models an R package 158 (and can do so from an arbitrary environment - with the caution 159 that locked environments should mostly be considered). 160 """ 161 162 _env = None 163 __rname__ = None 164 _translation = None 165 _rpy2r = None 166 _exported_names = None 167 _symbol_r2python = None 168 __version__ = None 169 __rdata__ = None 170 171 def __init__(self, env, name, translation={}, 172 exported_names=None, on_conflict='fail', 173 version=None, 174 symbol_r2python=default_symbol_r2python, 175 symbol_resolve=default_symbol_resolve): 176 """ Create a Python module-like object from an R environment, 177 using the specified translation if defined. 178 179 - env: R environment 180 - name: package name 181 - translation: `dict` with R names as keys and corresponding Python 182 names as values 183 - exported_names: `set` of names/symbols to expose to instance users 184 - on_conflict: 'fail' or 'warn' (default: 'fail') 185 - version: version string for the package 186 - symbol_r2python: function to convert R symbols into Python symbols. 187 The default translate `.` into `_`. 188 - symbol_resolve: function to check the Python symbols obtained 189 from `symbol_r2python`. 190 """ 191 192 super(Package, self).__init__(name) 193 self._env = env 194 self.__rname__ = name 195 self._translation = translation 196 mynames = tuple(self.__dict__) 197 self._rpy2r = {} 198 if exported_names is None: 199 exported_names = set(self._env.keys()) 200 self._exported_names = exported_names 201 self._symbol_r2python = symbol_r2python 202 self._symbol_resolve = symbol_resolve 203 self.__fill_rpy2r__(on_conflict=on_conflict) 204 self._exported_names = self._exported_names.difference(mynames) 205 self.__version__ = version 206 207 def __update_dict__(self, on_conflict='fail'): 208 """ Update the __dict__ according to what is in the R environment """ 209 for elt in self._rpy2r: 210 del(self.__dict__[elt]) 211 self._rpy2r.clear() 212 self.__fill_rpy2r__(on_conflict=on_conflict) 213 214 def __fill_rpy2r__(self, on_conflict='fail'): 215 """ Fill the attribute _rpy2r. 216 217 - on_conflict: 'fail' or 'warn' (default: 'fail') 218 """ 219 assert(on_conflict in ('fail', 'warn')) 220 221 name = self.__rname__ 222 223 (symbol_mapping, 224 conflicts, 225 resolutions) = _map_symbols( 226 self._env, 227 translation=self._translation, 228 symbol_r2python=self._symbol_r2python, 229 symbol_resolve=self._symbol_resolve 230 ) 231 msg_prefix = ('Conflict when converting R symbols' 232 ' in the package "%s"' 233 ' to Python symbols: \n-' % self.__rname__) 234 exception = LibraryError 235 _fix_map_symbols(symbol_mapping, 236 conflicts, 237 on_conflict, 238 msg_prefix, 239 exception) 240 symbol_mapping.update(resolutions) 241 reserved_pynames = set(dir(self)) 242 for rpyname, rnames in symbol_mapping.items(): 243 # last paranoid check 244 if len(rnames) > 1: 245 raise ValueError( 246 'Only one R name should be associated with %s ' 247 '(and we have %s)' % (rpyname, str(rnames)) 248 ) 249 rname = rnames[0] 250 if rpyname in reserved_pynames: 251 raise LibraryError('The symbol ' + rname + 252 ' in the package "' + name + '"' + 253 ' is conflicting with' + 254 ' a Python object attribute') 255 self._rpy2r[rpyname] = rname 256 if (rpyname != rname) and (rname in self._exported_names): 257 self._exported_names.remove(rname) 258 self._exported_names.add(rpyname) 259 try: 260 riobj = self._env[rname] 261 except rinterface.embedded.RRuntimeError as rre: 262 warn(str(rre)) 263 rpyobj = conversion.rpy2py(riobj) 264 if hasattr(rpyobj, '__rname__'): 265 rpyobj.__rname__ = rname 266 # TODO: shouldn't the original R name be also in the __dict__ ? 267 self.__dict__[rpyname] = rpyobj 268 269 def __repr__(self): 270 s = super(Package, self).__repr__() 271 return 'rpy2.robjects.packages.Package as a %s' % s 272 273 274# alias 275STF = SignatureTranslatedFunction 276 277 278class SignatureTranslatedPackage(Package): 279 """ R package in which the R functions had their signatures 280 'translated' (that this the named parameters were made to 281 to conform Python's rules for vaiable names).""" 282 def __fill_rpy2r__(self, on_conflict='fail'): 283 (super(SignatureTranslatedPackage, self) 284 .__fill_rpy2r__(on_conflict=on_conflict)) 285 for name, robj in self.__dict__.items(): 286 if isinstance(robj, rinterface.Sexp) and \ 287 robj.typeof == rinterface.RTYPES.CLOSXP: 288 self.__dict__[name] = STF( 289 self.__dict__[name], 290 on_conflict=on_conflict, 291 symbol_r2python=self._symbol_r2python, 292 symbol_resolve=self._symbol_resolve 293 ) 294 295 296# alias 297STP = SignatureTranslatedPackage 298 299 300class SignatureTranslatedAnonymousPackage(SignatureTranslatedPackage): 301 def __init__(self, string, name): 302 env = Environment() 303 reval(string, env) 304 super(SignatureTranslatedAnonymousPackage, self).__init__(env, 305 name) 306 307 308# alias 309STAP = SignatureTranslatedAnonymousPackage 310 311 312class InstalledSTPackage(SignatureTranslatedPackage): 313 @docstring_property(__doc__) 314 def __doc__(self): 315 doc = list(['Python representation of an R package.']) 316 if not self.__rname__: 317 doc.append('<No information available>') 318 else: 319 try: 320 doc.append(rhelp.docstring(self.__rname__, 321 self.__rname__ + '-package', 322 sections=['\\description'])) 323 except rhelp.HelpNotFoundError: 324 doc.append('[R help was not found]') 325 return os.linesep.join(doc) 326 327 def __fill_rpy2r__(self, on_conflict='fail'): 328 (super(SignatureTranslatedPackage, self) 329 .__fill_rpy2r__(on_conflict=on_conflict)) 330 for name, robj in self.__dict__.items(): 331 if isinstance(robj, rinterface.Sexp) and \ 332 robj.typeof == rinterface.RTYPES.CLOSXP: 333 self.__dict__[name] = DocumentedSTFunction( 334 self.__dict__[name], 335 packagename=self.__rname__ 336 ) 337 338 339class InstalledPackage(Package): 340 @docstring_property(__doc__) 341 def __doc__(self): 342 doc = list(['Python representation of an R package.', 343 'R arguments:', '']) 344 if not self.__rname__: 345 doc.append('<No information available>') 346 else: 347 try: 348 doc.append(rhelp.docstring(self.__rname__, 349 self.__rname__ + '-package', 350 sections=['\\description'])) 351 except rhelp.HelpNotFoundError: 352 doc.append('[R help was not found]') 353 return os.linesep.join(doc) 354 355 356class WeakPackage(Package): 357 """ 358 'Weak' R package, with which looking for symbols results in 359 a warning (and a None returned) whenever the desired symbol is 360 not found (rather than a traditional `AttributeError`). 361 """ 362 363 def __getattr__(self, name): 364 res = self.__dict__.get(name) 365 if res is None: 366 warnings.warn( 367 "The symbol '%s' is not in this R namespace/package." % name 368 ) 369 return res 370 371 372class LibraryError(ImportError): 373 """ Error occuring when importing an R library """ 374 pass 375 376 377class PackageNotInstalledError(LibraryError): 378 """ Error occuring because the R package to import is not installed.""" 379 pass 380 381 382class InstalledPackages(object): 383 """ R packages installed. """ 384 def __init__(self, lib_loc=None): 385 libraryiqr = _library(**{'lib.loc': lib_loc}) 386 lib_results_i = libraryiqr.do_slot('names').index('results') 387 self.lib_results = libraryiqr[lib_results_i] 388 self.nrows, self.ncols = self.lib_results.do_slot('dim') 389 self.colnames = self.lib_results.do_slot('dimnames')[1] # column names 390 self.lib_packname_i = self.colnames.index('Package') 391 392 def isinstalled(self, packagename: str): 393 if not isinstance(packagename, rinterface.StrSexpVector): 394 rinterface.StrSexpVector((packagename, )) 395 else: 396 if len(packagename) > 1: 397 raise ValueError("Only specify one package name at a time.") 398 nrows = self.nrows 399 lib_results, lib_packname_i = self.lib_results, self.lib_packname_i 400 for i in range(0+lib_packname_i*nrows, 401 nrows*(lib_packname_i+1), 402 1): 403 if lib_results[i] == packagename: 404 return True 405 return False 406 407 def __iter__(self): 408 """ Iterate through rows, yield tuples at each iteration """ 409 lib_results = self.lib_results 410 nrows, ncols = self.nrows, self.ncols 411 colrg = range(0, ncols) 412 for row_i in range(nrows): 413 yield tuple(lib_results[x*nrows+row_i] for x in colrg) 414 415 416def isinstalled(name: str, 417 lib_loc=None): 418 """ 419 Find whether an R package is installed 420 :param name: name of an R package 421 :param lib_loc: specific location for the R library (default: None) 422 423 :rtype: a :class:`bool` 424 """ 425 426 instapack = InstalledPackages(lib_loc) 427 return instapack.isinstalled(name) 428 429 430def importr(name: str, 431 lib_loc=None, 432 robject_translations={}, 433 signature_translation=True, 434 suppress_messages=True, 435 on_conflict='fail', 436 symbol_r2python=default_symbol_r2python, 437 symbol_resolve=default_symbol_resolve, 438 data=True): 439 """ Import an R package. 440 441 Arguments: 442 443 - name: name of the R package 444 445 - lib_loc: specific location for the R library (default: None) 446 447 - robject_translations: dict (default: {}) 448 449 - signature_translation: (True or False) 450 451 - suppress_message: Suppress messages R usually writes on the console 452 (defaut: True) 453 454 - on_conflict: 'fail' or 'warn' (default: 'fail') 455 456 - symbol_r2python: function to translate R symbols into Python symbols 457 458 - symbol_resolve: function to check the Python symbol obtained 459 from `symbol_r2python`. 460 461 - data: embed a PackageData objects under the attribute 462 name __rdata__ (default: True) 463 464 Return: 465 466 - an instance of class SignatureTranslatedPackage, or of class Package 467 468 """ 469 470 if not isinstalled(name, lib_loc=lib_loc): 471 raise PackageNotInstalledError( 472 'The R package "%s" is not installed.' % name 473 ) 474 475 if suppress_messages: 476 ok = quiet_require(name, lib_loc=lib_loc) 477 else: 478 ok = _require(name, 479 **{'lib.loc': rinterface.StrSexpVector((lib_loc, ))})[0] 480 if not ok: 481 raise LibraryError("The R package %s could not be imported" % name) 482 if _package_has_namespace(name, 483 _system_file(package=name)): 484 env = _get_namespace(name) 485 version = _get_namespace_version(name)[0] 486 exported_names = set(_get_namespace_exports(name)) 487 else: 488 env = _as_env(rinterface.StrSexpVector(['package:'+name, ])) 489 exported_names = None 490 version = None 491 492 if signature_translation: 493 pack = InstalledSTPackage(env, name, 494 translation=robject_translations, 495 exported_names=exported_names, 496 on_conflict=on_conflict, 497 version=version, 498 symbol_r2python=symbol_r2python, 499 symbol_resolve=symbol_resolve) 500 else: 501 pack = InstalledPackage(env, name, translation=robject_translations, 502 exported_names=exported_names, 503 on_conflict=on_conflict, 504 version=version, 505 symbol_r2python=symbol_r2python, 506 symbol_resolve=symbol_resolve) 507 if data: 508 if pack.__rdata__ is not None: 509 warn('While importing the R package "%s", the rpy2 Package object ' 510 'is masking a translated R symbol "__rdata__" already present' 511 % name) 512 pack.__rdata__ = PackageData(name, lib_loc=lib_loc) 513 514 return pack 515 516 517def data(package): 518 """ Return the PackageData for the given package.""" 519 return package.__rdata__ 520 521 522def wherefrom(symbol: str, 523 startenv: rinterface.SexpEnvironment = rinterface.globalenv): 524 """ For a given symbol, return the environment 525 this symbol is first found in, starting from 'startenv'. 526 """ 527 env = startenv 528 while True: 529 if symbol in env: 530 break 531 env = env.enclos 532 if env.rsame(rinterface.emptyenv): 533 break 534 return conversion.rpy2py(env) 535 536 537class ParsedCode(rinterface.ExprSexpVector): 538 pass 539 540 541class SourceCode(str): 542 543 _parsed = None 544 545 def parse(self): 546 if self._parsed is None: 547 self._parsed = ParsedCode(rinterface.parse(self)) 548 return self._parsed 549 550 def as_namespace(self, name): 551 """ Name for the namespace """ 552 return SignatureTranslatedAnonymousPackage(self, 553 name) 554