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