Skip to content

Commit

Permalink
rpy2.robjects.R.__call__() prints R warnings at the end of the evalua… (
Browse files Browse the repository at this point in the history
#1025)

* rpy2.robjects.R.__call__() prints R warnings at the end of the evaluation.

* Bump version number.

* tzlocal 5.0 appears to cause API-breaking changes.
  • Loading branch information
lgautier committed May 20, 2023
1 parent c6e5c1e commit 7a74a8f
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 56 deletions.
17 changes: 17 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,29 @@ New features
embedded R logged by :mod:`rpy2.rinterface_lib.embedded.logger`
at `INFO` and `DEBUG` levels.

- :meth:`rpy2.robjects.R.__call__` has 2 named arguments "visible" and
"print_r_warnings" to handle R "invisible" results and print R warnings
at the end of the evaluation. Invisible results happen when doing
`rpy2.robjects.r("x <- 1")`. R will return the value of x "invisibly".
The default are `invisible is True` and `print_r_warnings is True`.


Bugs fixed
----------

- "R magic" cells are now show R warnings at the end of the
evaluation of an R cell (#issue 226).

Changes
-------

- Evaluating a string as R code using :meth:`rpy2.robjects.R.__call__`
(e.g., `rpy2.robjects.r("1+2")`) now shows R warnings at the end
of the evaluation by default.

- :meth:`rpy2.robjects.R.__call__` returns the results invisibly by default
(see section "New features" for this release).


Release 3.5.11
==============
Expand Down
4 changes: 2 additions & 2 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@
# built documents.
#
# The short X.Y version.
version = '3.5.11'
version = '3.5.12'
# The full version, including alpha/beta/rc tags.
release = '3.5.11'
release = '3.5.12'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies = [
"cffi>=1.10.0",
"jinja2",
"pytz",
"tzlocal",
"tzlocal<5.0",
"packaging;platform_system=='Windows'",
"typing-extensions;python_version<'3.8'"
]
Expand Down
2 changes: 1 addition & 1 deletion rpy2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version_vector__ = (3, 5, 11)
__version_vector__ = (3, 5, 12)

__version__ = '.'.join(str(x) for x in __version_vector__)
83 changes: 43 additions & 40 deletions rpy2/ipython/rmagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@
import textwrap
import typing

# numpy and rpy2 imports
import rpy2.rinterface_lib.callbacks

import rpy2.rinterface as ri
import rpy2.rinterface_lib.callbacks

import rpy2.rinterface_lib.openrlib
import rpy2.robjects as ro
import rpy2.robjects.packages as rpacks
Expand Down Expand Up @@ -125,32 +125,6 @@ def _get_converter(template_converter=template_converter):
template=template_converter)


# TODO: Something like this could be part of the rpy2 API.
def _print_deferred_warnings() -> None:
"""Print R warning messages.
rpy2's default pattern add a prefix per warning lines.
This should be revised. In the meantime, we clean it
at least for the R magic.
"""

with contextlib.ExitStack() as stack:
stack.enter_context(
rpy2.rinterface_lib.openrlib.rlock
)
stack.enter_context(
rpy2.rinterface_lib.callbacks.obj_in_module(
rpy2.rinterface_lib.callbacks,
'_WRITECONSOLE_EXCEPTION_LOG',
'%s')
)
try:
ro.r('.Internal(printDeferredWarnings())')
except (ri.embedded.RRuntimeError, ValueError):
# TODO: report this in a logger.
pass


ipy_template_converter = _get_ipython_template_converter(template_converter,
numpy=numpy,
pandas=pandas)
Expand Down Expand Up @@ -400,13 +374,27 @@ def eval(self, code):
withVisible is a LISPy R function).
"""
with contextlib.ExitStack() as stack:
obj_in_module = (rpy2.rinterface_lib
.callbacks.obj_in_module)
if self.cache_display_data:
stack.enter(
rpy2.rinterface_lib
.callbacks.obj_in_module(rpy2.rinterface_lib.callbacks,
'consolewrite_print',
self.write_console_regular)
stack.enter_context(
obj_in_module(
rpy2.rinterface_lib.callbacks,
'consolewrite_print',
self.write_console_regular
)
)
stack.enter_context(
obj_in_module(rpy2.rinterface_lib.callbacks,
'consolewrite_warnerror',
self.write_console_regular)
)
stack.enter_context(
obj_in_module(
rpy2.rinterface_lib.callbacks,
'_WRITECONSOLE_EXCEPTION_LOG',
'%s')
)
try:
# Need the newline in case the last line in code is a comment.
r_expr = ri.parse(code)
Expand All @@ -419,7 +407,7 @@ def eval(self, code):
raise RInterpreterError(code, str(exception),
warning_or_other_msg)
finally:
_print_deferred_warnings()
ro._print_deferred_warnings()
text_output = self.flush()
return text_output, value, visible[0]

Expand Down Expand Up @@ -956,14 +944,29 @@ def R(self, line, cell=None, local_ns=None):
text_output += text_result
if visible:
with contextlib.ExitStack() as stack:
obj_in_module = (rpy2.rinterface_lib
.callbacks
.obj_in_module)
if self.cache_display_data:
stack.enter_context(
rpy2.rinterface_lib
.callbacks
.obj_in_module(rpy2.rinterface_lib
.callbacks,
'consolewrite_print',
self.write_console_regular))
obj_in_module(rpy2.rinterface_lib
.callbacks,
'consolewrite_print',
self.write_console_regular)
)
stack.enter_context(
obj_in_module(
rpy2.rinterface_lib.callbacks,
'consolewrite_warnerror',
self.write_console_regular
)
)
stack.enter_context(
obj_in_module(
rpy2.rinterface_lib.callbacks,
'_WRITECONSOLE_EXCEPTION_LOG',
'%s')
)
cell_display(result, args)
text_output += self.flush()

Expand Down
3 changes: 2 additions & 1 deletion rpy2/rinterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def evalr_expr_with_visible(
envir: typing.Union[
None,
'SexpEnvironment'] = None
) -> sexp.Sexp:
) -> 'ListSexpVector':
"""Evaluate an R expression and return value and visibility flag.
:param expr: An R expression.
Expand Down Expand Up @@ -182,6 +182,7 @@ def evalr_expr_with_visible(
if error_occured[0]:
raise embedded.RRuntimeError(_rinterface._geterrmessage())
res = conversion._cdata_to_rinterface(r_res)
assert isinstance(res, ListSexpVector)
return res


Expand Down
75 changes: 64 additions & 11 deletions rpy2/robjects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
"""

import array
import contextlib
import os
import types
import typing
import rpy2.rinterface as rinterface
import rpy2.rinterface_lib.embedded
import rpy2.rinterface_lib.openrlib
import rpy2.rlike.container as rlc

from rpy2.robjects.robject import RObjectMixin, RObject
Expand Down Expand Up @@ -66,6 +69,19 @@
NULL = rinterface.NULL


# TODO: Something like this could be part of the rpy2 API.
def _print_deferred_warnings() -> None:
"""Print R warning messages.
rpy2's default pattern add a prefix per warning lines.
This should be revised. In the meantime, we clean it
at least for the R magic.
"""

with rpy2.rinterface_lib.openrlib.rlock:
rinterface.evalr('.Internal(printDeferredWarnings())')


def reval(string, envir=_globalenv):
""" Evaluate a string as R code
- string: a string
Expand Down Expand Up @@ -417,14 +433,18 @@ class R(object):
Singleton representing the embedded R running.
"""
_instance = None
# Default for the evaluation
_print_r_warnings: bool = True
_invisible: bool = True


def __new__(cls):
if cls._instance is None:
rinterface.initr_simple()
cls._instance = object.__new__(cls)
return cls._instance

def __getattribute__(self, attr):
def __getattribute__(self, attr: str) -> object:
try:
return super(R, self).__getattribute__(attr)
except AttributeError as ae:
Expand All @@ -435,29 +455,62 @@ def __getattribute__(self, attr):
except LookupError:
raise AttributeError(orig_ae)

def __getitem__(self, item):
def __getitem__(self, item: str) -> object:
res = _globalenv.find(item)
res = conversion.get_conversion().rpy2py(res)
if hasattr(res, '__rname__'):
res.__rname__ = item
return res

# TODO: check that this is properly working
def __cleanup__(self):
def __cleanup__(self) -> None:
rinterface.embedded.endr(0)
del(self)

def __str__(self):
version = self['version']
def __str__(self) -> str:
s = [super(R, self).__str__()]
s.extend('%s: %s' % (n, val[0])
for n, val in zip(version.names, version))
version = self['version']
version_k: typing.Tuple[str, ...] = tuple(version.names) # type: ignore
version_v: typing.Tuple[str, ...] = tuple(
x[0] for x in version # type: ignore
)
for key, val in zip(version_k, version_v):
s.extend('%s: %s' % (key, val))
return os.linesep.join(s)

def __call__(self, string):
p = rinterface.parse(string)
res = self.eval(p)
return conversion.get_conversion().rpy2py(res)
def __call__(self, string: str,
invisible: typing.Optional[bool] = None,
print_r_warnings: typing.Optional[bool] = None) -> object:
"""Evaluate a string as R code.
:param string: A string with R code
:param invisible: evaluate the R expression handling R's
invisibility flag. When `True` expressions meant to return
an "invisible" result (for example, `x <- 1`) will return
None. The default is `None`, in which case the attribute
_invisible is used.
:param print_r_warning: When `True` the R deferred warnings
are printed using the R callback function. The default is
`None`, in which case the attribute _print_r_warning
is used.
:return: The value returned by R after rpy2 conversion."""
r_expr = rinterface.parse(string)
if invisible is None:
invisible = self._invisible
if invisible:
res, visible = rinterface.evalr_expr_with_visible( # type: ignore
r_expr
)
if not visible[0]: # type: ignore
res = None
else:
res = rinterface.evalr_expr(r_expr)
if print_r_warnings is None:
print_r_warnings = self._print_r_warnings
if print_r_warnings:
_print_deferred_warnings()
return (None if res is None
else conversion.get_conversion().rpy2py(res))


r = R()
Expand Down

0 comments on commit 7a74a8f

Please sign in to comment.