Skip to content

Commit

Permalink
Cleanup a bit ipython code that displays figures. (#1003)
Browse files Browse the repository at this point in the history
* Cleanup a bit ipython code that displays figures.

Fixes deprecation warnings. Add jpeg support.

Also bump target version for tidyr and dbplyr to more recent releases.
  • Loading branch information
lgautier committed Mar 5, 2023
1 parent 8b84aa6 commit 5dc1885
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 46 deletions.
3 changes: 3 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ New features

- :mod:`rpy2.situation` reports cffi interface type information.

- The ipython/jupyter R magic can now have `jpeg` as a default graphics format
for static figures in addition to `png` and `svg`.

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

Expand Down
141 changes: 101 additions & 40 deletions rpy2/ipython/rmagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
import sys
import tempfile
from glob import glob
from os import stat
import os
from shutil import rmtree
import textwrap
import typing
Expand Down Expand Up @@ -88,6 +88,7 @@

# IPython imports.

import IPython.display # type: ignore
from IPython.core import displaypub # type: ignore
from IPython.core.magic import (Magics, # type: ignore
magics_class,
Expand All @@ -102,6 +103,10 @@

template_converter = get_conversion()

DEVICES_STATIC_RASTER = {'png', 'jpeg'}
DEVICES_STATIC = DEVICES_STATIC_RASTER | {'svg'}
DEVICES_SUPPORTED = DEVICES_STATIC | {'X11'}


def _get_ipython_template_converter(template_converter=template_converter,
numpy=numpy, pandas=pandas):
Expand Down Expand Up @@ -227,6 +232,54 @@ def _parse_input_argument(arg: str) -> typing.Tuple[str, str]:
# return res


def display_figures(graph_dir, format='png'):
"""Iterator to display PNG figures generated by R.
graph_dir : str
A directory where R figures are to be searched.
The iterator yields the image objects."""

assert format in DEVICES_STATIC_RASTER
for imgfile in sorted(glob(os.path.join(graph_dir, f'Rplots*{format}'))):
if os.stat(imgfile).st_size >= 1000:
img = IPython.display.Image(filename=imgfile)
IPython.display.display(img,
metadata={})
# TODO: Synchronization in the console (though it's a bandaid, not a
# real solution).
sys.stdout.flush()
sys.stderr.flush()
yield img


def display_figures_svg(graph_dir, isolate_svgs=True):
"""Display SVG figures generated by R.
graph_dir : str
A directory where R figures are to be searched.
isolate_svgs : bool
Enable SVG namespace isolation in metadata.
The iterator yields the image object."""

# as onefile=TRUE, there is only one .svg file
imgfile = "%s/Rplot.svg" % graph_dir
# Cairo creates an SVG file every time R is called
# -- empty ones are not published
if os.stat(imgfile).st_size >= 1000:
img = IPython.display.SVG(filename=imgfile),
IPython.display.display_svg(
img,
metadata={'image/svg+xml': dict(isolated=True)} if isolate_svgs else {}
)
# TODO: Synchronization in the console (though it's a bandaid, not a
# real solution).
sys.stdout.flush()
sys.stderr.flush()
yield img


@magics_class
class RMagics(Magics):
"""A set of magics useful for interactive work with R via rpy2.
Expand All @@ -247,12 +300,12 @@ def __init__(self, shell, converter=converter,
If True, the published results of the final call to R are
cached in the variable 'display_cache'.
device : ['png', 'X11', 'svg']
device : ['png', 'jpeg', 'X11', 'svg']
Device to be used for plotting.
Currently only 'png', 'X11' and 'svg' are supported,
with 'png' and 'svg' being most useful in the notebook,
and 'X11' allowing interactive plots in the terminal.
Currently only 'png', 'jpeg', 'X11' and 'svg' are supported,
with 'X11' allowing interactive plots on a locally-running jupyter,
and the other allowing to visualize R figure generated on a remote
jupyter server/kernel.
"""
super(RMagics, self).__init__(shell)
self.cache_display_data = cache_display_data
Expand All @@ -267,17 +320,18 @@ def set_R_plotting_device(self, device):
must be installed. Because Cairo forces "onefile=TRUE",
it is not posible to include multiple plots per cell.
:param device: ['png', 'X11', 'svg']
Device to be used for plotting.
Currently only "png" and "X11" are supported,
with 'png' and 'svg' being most useful in the notebook,
and 'X11' allowing interactive plots in the terminal.
:param device: ['png', 'jpeg', 'X11', 'svg']
Device to be used for plotting.
Currently only 'png', 'jpeg', 'X11' and 'svg' are supported,
with 'X11' allowing interactive plots on a locally-running jupyter,
and the other allowing to visualize R figure generated on a remote
jupyter server/kernel.
"""
device = device.strip()
if device not in ['png', 'X11', 'svg']:
if device not in DEVICES_SUPPORTED:
raise ValueError(
"device must be one of ['png', 'X11' 'svg'], got '%s'", device)
f'device must be one of {DEVICES_SUPPORTED}, got "{device}"'
)
if device == 'svg':
try:
self.cairo = rpacks.importr('Cairo')
Expand All @@ -301,8 +355,8 @@ def set_R_plotting_device(self, device):
@line_magic
def Rdevice(self, line):
"""
Change the plotting device R uses to one of ['png', 'X11', 'svg'].
"""
Change the plotting device R uses to one of {}.
""".format(DEVICES_SUPPORTED)
self.set_R_plotting_device(line.strip())

def eval(self, code):
Expand Down Expand Up @@ -534,7 +588,7 @@ def setup_graphics(self, args):
args.res = 72

plot_arg_names = ['width', 'height', 'pointsize', 'bg', 'type']
if self.device == 'png':
if self.device in DEVICES_STATIC_RASTER:
plot_arg_names += ['units', 'res']

argdict = {}
Expand All @@ -544,19 +598,20 @@ def setup_graphics(self, args):
argdict[name] = val

tmpd = None
if self.device in ['png', 'svg']:
if self.device in DEVICES_STATIC:
# Create a temporary directory for R graphics output
# TODO: Do we want to capture file output for other device types
# other than svg & png?
tmpd = tempfile.mkdtemp()
tmpd_fix_slashes = tmpd.replace('\\', '/')

if self.device == 'png':
# Note: that %% is to pass into R for interpolation there
grdevices.png("%s/Rplots%%03d.png" % tmpd_fix_slashes,
**argdict)
if self.device in DEVICES_STATIC_RASTER:
# Note: that %% is to pass into R for interpolation there.
rfunc = getattr(grdevices, self.device)
rfunc(f'{tmpd_fix_slashes}/Rplots%%03d.{self.device}',
**argdict)
elif self.device == 'svg':
self.cairo.CairoSVG("%s/Rplot.svg" % tmpd_fix_slashes,
self.cairo.CairoSVG(f'{tmpd_fix_slashes}/Rplot.svg',
**argdict)

elif self.device == 'X11':
Expand All @@ -570,18 +625,22 @@ def setup_graphics(self, args):
else:
# TODO: This isn't actually an R interpreter error...
raise RInterpreterError(
'device must be one of ("png", "X11", "svg")')
f'device must be one of {DEVICES_SUPPORTED}')

return tmpd

def publish_graphics(self, graph_dir, isolate_svgs=True):
"""Wrap graphic file data for presentation in IPython
"""Wrap graphic file data for presentation in IPython.
This method is deprecated. Use `display_figures` or
'display_figures_svg` instead.
graph_dir : str
Probably provided by some tmpdir call
isolate_svgs : bool
Enable SVG namespace isolation in metadata"""

warnings.warn('Use method fetch_figures.', DeprecationWarning)
# read in all the saved image files
images = []
display_data = []
Expand All @@ -591,15 +650,15 @@ def publish_graphics(self, graph_dir, isolate_svgs=True):

if self.device == 'png':
for imgfile in sorted(glob('%s/Rplots*png' % graph_dir)):
if stat(imgfile).st_size >= 1000:
if os.stat(imgfile).st_size >= 1000:
with open(imgfile, 'rb') as fh_img:
images.append(fh_img.read())
else:
# as onefile=TRUE, there is only one .svg file
imgfile = "%s/Rplot.svg" % graph_dir
# Cairo creates an SVG file every time R is called
# -- empty ones are not published
if stat(imgfile).st_size >= 1000:
if os.stat(imgfile).st_size >= 1000:
with open(imgfile, 'rb') as fh_img:
images.append(fh_img.read().decode())

Expand All @@ -613,7 +672,7 @@ def publish_graphics(self, graph_dir, isolate_svgs=True):
# Flush text streams before sending figures, helps a little with
# output.
for image in images:
# Synchronization in the console (though it's a bandaid, not a
# TODO: Synchronization in the console (though it's a bandaid, not a
# real solution).
sys.stdout.flush()
sys.stderr.flush()
Expand Down Expand Up @@ -850,6 +909,7 @@ def R(self, line, cell=None, local_ns=None):
tmpd = self.setup_graphics(args)

text_output = ''
display_data = []
try:
if line_mode:
for line in code.split(';'):
Expand Down Expand Up @@ -882,24 +942,25 @@ def R(self, line, cell=None, local_ns=None):
print(e.err)
raise e
finally:
if self.device in ['png', 'svg']:
if self.device in DEVICES_STATIC:
ro.r('dev.off()')
if text_output:
# display_data.append(('RMagic.R', {'text/plain':text_output}))
displaypub.publish_display_data(
data={'text/plain': text_output}, source='RMagic.R')
# publish the R images
if self.device in ['png', 'svg']:
display_data, md = self.publish_graphics(
tmpd, args.isolate_svgs
)

for tag, disp_d in display_data:
displaypub.publish_display_data(data=disp_d, source=tag,
metadata=md)
source='RMagic.R',
data={'text/plain': text_output})
# publish figures generated by R.
if self.device in DEVICES_STATIC_RASTER:
for _ in display_figures(tmpd, format=self.device):
if self.cache_display_data:
display_data.append(_)
elif self.device == 'svg':
for _ in display_figures_svg(tmpd):
if self.cache_display_data:
display_data.append(_)

# kill the temporary directory - currently created only for "svg"
# and "png" (else it's None)
# and ("png"|"jpeg") (else it's None).
if tmpd:
rmtree(tmpd)

Expand Down
7 changes: 4 additions & 3 deletions rpy2/rinterface_lib/embedded.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,10 @@ def set_initoptions(options: typing.Tuple[str]) -> None:
global _options
for x in options:
assert isinstance(x, str)
logger.info('Setting options to initialize R: {}'
.format(', '.join(options)))
_options = tuple(options)
with openrlib.rlock:
logger.info('Setting options to initialize R: {}'
.format(', '.join(options)))
_options = tuple(options)


def get_initoptions() -> typing.Tuple[str, ...]:
Expand Down
2 changes: 1 addition & 1 deletion rpy2/robjects/lib/dbplyr.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
version=dbplyr.__version__,
symbol_r2python=dbplyr._symbol_r2python,
symbol_resolve=dbplyr._symbol_resolve)
TARGET_VERSION = '2.0'
TARGET_VERSION = '2.2.'
if not dbplyr.__version__.startswith(TARGET_VERSION):
warnings.warn(
'This was designed againt dbplyr versions starting with %s'
Expand Down
2 changes: 1 addition & 1 deletion rpy2/robjects/lib/tidyr.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
warnings.simplefilter("ignore")
tidyr = importr('tidyr', on_conflict="warn")

TARGET_VERSION = '1.1.'
TARGET_VERSION = '1.2.'

if not tidyr.__version__.startswith(TARGET_VERSION):
warnings.warn(
Expand Down
2 changes: 1 addition & 1 deletion rpy2/tests/ipython/test_rmagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def ipython_with_magic():
ip = get_ipython()
# This is just to get a minimally modified version of the changes
# working
ip.magic('load_ext rpy2.ipython')
ip.run_line_magic('load_ext', 'rpy2.ipython')
return ip


Expand Down

0 comments on commit 5dc1885

Please sign in to comment.