1import enum
2import os
3import sys
4import typing
5import warnings
6from rpy2.rinterface_lib import openrlib
7from rpy2.rinterface_lib import callbacks
8
9ffi = openrlib.ffi
10
11_options = ('rpy2', '--quiet', '--no-save')  # type: typing.Tuple[str, ...]
12_DEFAULT_C_STACK_LIMIT = -1
13rpy2_embeddedR_isinitialized = 0x00
14rstart = None
15
16
17# TODO: move initialization-related code to _rinterface ?
18class RPY_R_Status(enum.Enum):
19    """Possible status for the embedded R."""
20    INITIALIZED = 0x01
21    BUSY = 0x02
22    ENDED = 0x04
23
24
25def set_initoptions(options: typing.Tuple[str]) -> None:
26    """Set initialization options for the embedded R.
27
28    :param:`options` A tuple of string with the options
29    (e.g., '--verbose', '--quiet').
30    """
31    if rpy2_embeddedR_isinitialized:
32        raise RuntimeError('Options can no longer be set once '
33                           'R is initialized.')
34    global _options
35    for x in options:
36        assert isinstance(x, str)
37    _options = tuple(options)
38
39
40def get_initoptions() -> typing.Tuple[str, ...]:
41    """Get the initialization options for the embedded R."""
42    return _options
43
44
45def isinitialized() -> bool:
46    """Is the embedded R initialized."""
47    return bool(rpy2_embeddedR_isinitialized & RPY_R_Status.INITIALIZED.value)
48
49
50def _setinitialized() -> None:
51    """Set the embedded R as initialized.
52
53    This may result in a later segfault if used with the embedded R has not
54    been initialized. You should not have to use it."""
55    global rpy2_embeddedR_isinitialized
56    rpy2_embeddedR_isinitialized = RPY_R_Status.INITIALIZED.value
57
58
59def isready() -> bool:
60    """Is the embedded R ready for use."""
61    INITIALIZED = RPY_R_Status.INITIALIZED
62    return bool(
63        rpy2_embeddedR_isinitialized == INITIALIZED.value
64    )
65
66
67def assert_isready() -> None:
68    """Assert whether R is ready (initialized).
69
70    Raises an RNotReadyError if it is not."""
71    if not isready():
72        raise RNotReadyError(
73            'The embedded R is not ready to use.')
74
75
76class RNotReadyError(Exception):
77    """Embedded R is not ready to use."""
78    pass
79
80
81class RRuntimeError(Exception):
82    """Error generated by R."""
83    pass
84
85
86def _setcallback(rlib, rlib_symbol: str,
87                 callbacks,
88                 callback_symbol: str) -> None:
89    """Set R callbacks."""
90    if callback_symbol is None:
91        new_callback = ffi.NULL
92    else:
93        new_callback = getattr(callbacks, callback_symbol)
94    setattr(rlib, rlib_symbol, new_callback)
95
96
97CALLBACK_INIT_PAIRS = (('ptr_R_WriteConsoleEx', '_consolewrite_ex'),
98                       ('ptr_R_WriteConsole', None),
99                       ('ptr_R_ShowMessage', '_showmessage'),
100                       ('ptr_R_ReadConsole', '_consoleread'),
101                       ('ptr_R_FlushConsole', '_consoleflush'),
102                       ('ptr_R_ResetConsole', '_consolereset'),
103                       ('ptr_R_ChooseFile', '_choosefile'),
104                       ('ptr_R_ShowFiles', '_showfiles'),
105                       ('ptr_R_CleanUp', '_cleanup'),
106                       ('ptr_R_ProcessEvents', '_processevents'),
107                       ('ptr_R_Busy', '_busy'))
108
109
110# TODO: can init_once() be used here ?
111def _initr(
112        interactive: bool = True,
113        _want_setcallbacks: bool = True,
114        _c_stack_limit: int = _DEFAULT_C_STACK_LIMIT
115) -> typing.Optional[int]:
116
117    rlib = openrlib.rlib
118    ffi_proxy = openrlib.ffi_proxy
119    if (
120            ffi_proxy.get_ffi_mode(openrlib._rinterface_cffi)
121            ==
122            ffi_proxy.InterfaceType.ABI
123    ):
124        callback_funcs = callbacks
125    else:
126        callback_funcs = rlib
127
128    with openrlib.rlock:
129        if isinitialized():
130            return None
131        elif openrlib.R_HOME is None:
132            raise ValueError('openrlib.R_HOME cannot be None.')
133        elif openrlib.rlib.R_NilValue != ffi.NULL:
134            warnings.warn(
135                'R was initialized outside of rpy2 (R_NilValue != NULL). '
136                'Trying to use it nevertheless.'
137            )
138            _setinitialized()
139            return None
140        os.environ['R_HOME'] = openrlib.R_HOME
141        options_c = [ffi.new('char[]', o.encode('ASCII')) for o in _options]
142        n_options = len(options_c)
143        n_options_c = ffi.cast('int', n_options)
144
145        # TODO: Conditional in C code
146        rlib.R_SignalHandlers = 0
147
148        # Instead of calling Rf_initEmbeddedR which breaks threaded context
149        # perform the initialization manually to set R_CStackLimit before
150        # calling setup_Rmainloop(), see:
151        # https://github.com/rpy2/rpy2/issues/729
152        rlib.Rf_initialize_R(n_options_c, options_c)
153        if _c_stack_limit:
154            rlib.R_CStackLimit = ffi.cast('uintptr_t', _c_stack_limit)
155        rlib.R_Interactive = True
156        rlib.setup_Rmainloop()
157
158        _setinitialized()
159
160        rlib.R_Interactive = interactive
161
162        # TODO: Conditional definition in C code
163        #   (Aqua, TERM, and TERM not "dumb")
164        rlib.R_Outputfile = ffi.NULL
165        rlib.R_Consolefile = ffi.NULL
166
167        if _want_setcallbacks:
168            for rlib_symbol, callback_symbol in CALLBACK_INIT_PAIRS:
169                _setcallback(rlib, rlib_symbol,
170                             callback_funcs, callback_symbol)
171
172    return 1
173
174
175def endr(fatal: int) -> None:
176    global rpy2_embeddedR_isinitialized
177    rlib = openrlib.rlib
178    with openrlib.rlock:
179        if rpy2_embeddedR_isinitialized & RPY_R_Status.ENDED.value:
180            return
181        rlib.R_dot_Last()
182        rlib.R_RunExitFinalizers()
183        rlib.Rf_KillAllDevices()
184        rlib.R_CleanTempDir()
185        rlib.R_gc()
186        rlib.Rf_endEmbeddedR(fatal)
187        rpy2_embeddedR_isinitialized ^= RPY_R_Status.ENDED.value
188
189
190_REFERENCE_TO_R_SESSIONS = 'https://github.com/rstudio/reticulate/issues/98'
191_R_SESSION_INITIALIZED = 'R_SESSION_INITIALIZED'
192_PYTHON_SESSION_INITIALIZED = 'PYTHON_SESSION_INITIALIZED'
193
194
195def get_r_session_status(r_session_init=None) -> dict:
196    """Return information about the R session, if available.
197
198    Information about the R session being already initialized can be
199    communicated by an environment variable exported by the process that
200    initialized it. See discussion at:
201    %s
202    """ % _REFERENCE_TO_R_SESSIONS
203
204    res = {'current_pid': os.getpid()}
205
206    if r_session_init is None:
207        r_session_init = os.environ.get(_R_SESSION_INITIALIZED)
208    if r_session_init:
209        for item in r_session_init.split(':'):
210            try:
211                key, value = item.split('=', 1)
212            except ValueError:
213                warnings.warn(
214                    'The item %s in %s should be of the form key=value.' %
215                    (item, _R_SESSION_INITIALIZED)
216                )
217            res[key] = value
218    return res
219
220
221def is_r_externally_initialized() -> bool:
222    r_status = get_r_session_status()
223    return str(r_status['current_pid']) == str(r_status.get('PID'))
224
225
226def set_python_process_info() -> None:
227    """Set information about the Python process in an environment variable.
228
229    See discussion at:
230    %s
231    """ % _REFERENCE_TO_R_SESSIONS
232
233    info = (('current_pid', os.getpid()),
234            ('sys.executable', sys.executable))
235    info_string = ':'.join('%s=%s' % x for x in info)
236    os.environ[_PYTHON_SESSION_INITIALIZED] = info_string
237