Skip to content

Commit

Permalink
Merge branch 'hotfix/1.7.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
iainrussell committed May 18, 2021
2 parents 8cc2d3f + a1c776d commit 7bdc47d
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 107 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
Changelog for Metview's Python interface
========================================

1.7.2
------------------
- new argument to setoutput(plot_widget=) - default True, set False to allow images to be saved into the notebook
- multi-page plots in Jupyter notebooks now contain the animation controls by default


1.7.1
------------------
- added automatic play and speed controls to animated plots in Jupyter notebooks
Expand Down
2 changes: 1 addition & 1 deletion metview/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# requires a Python 3 interpreter
import sys

if sys.version_info[0] < 3:
if sys.version_info[0] < 3: # pragma: no cover
raise EnvironmentError(
"Metview's Python interface requires Python 3. You are using Python "
+ repr(sys.version_info)
Expand Down
208 changes: 109 additions & 99 deletions metview/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import numpy as np


__version__ = "1.7.1"
__version__ = "1.7.2"


def string_from_ffi(s):
Expand All @@ -45,7 +45,7 @@ def __init__(self):
self.debug = os.environ.get("METVIEW_PYTHON_DEBUG", "0") == "1"

# check whether we're in a running Metview session
if "METVIEW_TITLE_PROD" in os.environ:
if "METVIEW_TITLE_PROD" in os.environ: # pragma: no cover
self.persistent_session = True
self.info_section = {"METVIEW_LIB": os.environ["METVIEW_LIB"]}
return
Expand All @@ -54,7 +54,7 @@ def __init__(self):
import time
import subprocess

if self.debug:
if self.debug: # pragma: no cover
print("MetviewInvoker: Invoking Metview")
self.persistent_session = False
self.metview_replied = False
Expand All @@ -77,14 +77,14 @@ def __init__(self):
env_file.name,
str(pid),
]
if self.debug:
if self.debug: # pragma: no cover
metview_flags.insert(2, "-slog")
print("Starting Metview using these command args:")
print(metview_flags)

try:
subprocess.Popen(metview_flags)
except Exception as exp:
except Exception as exp: # pragma: no cover
print(
"Could not run the Metview executable ('" + metview_startup_cmd + "'); "
"check that the binaries for Metview (version 5 at least) are installed "
Expand All @@ -99,7 +99,7 @@ def __init__(self):
):
time.sleep(0.001)

if not (self.metview_replied):
if not (self.metview_replied): # pragma: no cover
raise Exception(
'Command "metview" did not respond within '
+ str(self.metview_startup_timeout)
Expand All @@ -121,7 +121,7 @@ def __init__(self):
def destroy(self):
"""Kills the Metview session. Raises an exception if it could not do it."""

if self.persistent_session:
if self.persistent_session: # pragma: no cover
return

if self.metview_replied:
Expand Down Expand Up @@ -188,7 +188,7 @@ def restore_signal_handlers(self):
# MacOS systems
lib = ffi.dlopen(os.path.join(mv_lib, "libMvMacro"))

except Exception as exp:
except Exception as exp: # pragma: no cover
print(
"Error loading Metview/libMvMacro. LD_LIBRARY_PATH="
+ os.environ.get("LD_LIBRARY_PATH", "")
Expand Down Expand Up @@ -597,7 +597,7 @@ def to_dataset(self, **kwarg):
# soft dependency on cfgrib
try:
import xarray as xr
except ImportError:
except ImportError: # pragma: no cover
print("Package xarray not found. Try running 'pip install xarray'.")
raise
dataset = xr.open_dataset(self.url(), engine="cfgrib", backend_kwargs=kwarg)
Expand Down Expand Up @@ -639,7 +639,7 @@ def __init__(self, val_pointer):
def to_dataframe(self):
try:
import pandas as pd
except ImportError:
except ImportError: # pragma: no cover
print("Package pandas not found. Try running 'pip install pandas'.")
raise

Expand All @@ -665,7 +665,7 @@ def to_dataset(self):
# soft dependency on xarray
try:
import xarray as xr
except ImportError:
except ImportError: # pragma: no cover
print("Package xarray not found. Try running 'pip install xarray'.")
raise
dataset = xr.open_dataset(self.url())
Expand All @@ -679,7 +679,7 @@ def __init__(self, val_pointer):
def to_dataframe(self):
try:
import pandas as pd
except ImportError:
except ImportError: # pragma: no cover
print("Package pandas not found. Try running 'pip install pandas'.")
raise

Expand All @@ -700,7 +700,7 @@ def __init__(self, val_pointer):
def to_dataframe(self):
try:
import pandas as pd
except ImportError:
except ImportError: # pragma: no cover
print("Package pandas not found. Try running 'pip install pandas'.")
raise

Expand Down Expand Up @@ -867,7 +867,7 @@ def vector_from_metview(val):
elif s == 8:
nptype = np.float64
b = lib.p_vector_double_array(vec)
else:
else: # pragma: no cover
raise Exception("Metview vector data type cannot be handled: ", s)

bsize = n * s
Expand Down Expand Up @@ -1080,28 +1080,15 @@ def merge(*args):
class Plot:
def __init__(self):
self.plot_to_jupyter = False
self.plot_widget = True
self.jupyter_args = {}

def __call__(self, *args, **kwargs):
# if animate=True is supplied, then create a Jupyter animation
if kwargs.get("animate", False):
return animate(args, kwargs)

# otherwise create a single static plot
if self.plot_to_jupyter:
f, tmp = tempfile.mkstemp(".png")
os.close(f)

base, ext = os.path.splitext(tmp)

output_args = {"output_name": base, "output_name_first_page_number": "off"}
output_args.update(self.jupyter_args)
met_setoutput(png_output(output_args))
met_plot(*args)

image = Image(tmp)
os.unlink(tmp)
return image
if self.plot_to_jupyter: # pragma: no cover
if self.plot_widget:
return plot_to_notebook(args, **kwargs)
else:
return plot_to_notebook_return_image(args, **kwargs)
else:
map_outputs = {
"png": png_output,
Expand All @@ -1122,14 +1109,9 @@ def __call__(self, *args, **kwargs):

# animate - only usable within Jupyter notebooks
# generates a widget allowing the user to select between plot frames
def animate(*args, **kwargs):

if not plot.plot_to_jupyter:
raise EnvironmentError(
"animate() can only be used after calling set_output('jupyter')"
)
def plot_to_notebook(*args, **kwargs): # pragma: no cover

import ipywidgets as widgets
animation_mode = kwargs.get("animate", "auto") # True, False or "auto"

# create all the widgets first so that the 'waiting' label is at the bottom
image_widget = widgets.Image(
Expand All @@ -1138,47 +1120,9 @@ def animate(*args, **kwargs):
# height=400,
)

frame_widget = widgets.IntSlider(
value=1,
min=1,
max=1,
step=1,
description="Frame:",
disabled=False,
continuous_update=True,
readout=True,
)

play_widget = widgets.Play(
value=1,
min=1,
max=1,
step=1,
interval=500,
description="Play animation",
disabled=False,
)

speed_widget = widgets.IntSlider(
value=3,
min=1,
max=20,
step=1,
description="Speed",
disabled=False,
continuous_update=True,
readout=True,
)

widgets.jslink((play_widget, "value"), (frame_widget, "value"))
play_and_speed_widget = widgets.HBox([play_widget, speed_widget])
controls = widgets.VBox([frame_widget, play_and_speed_widget])

controls.layout.visibility = "hidden"
image_widget.layout.visibility = "hidden"
waitl_widget = widgets.Label(value="Generating plots....")
frame_widget.layout.width = "800px"
display(image_widget, controls, waitl_widget)
display(image_widget, waitl_widget)

# plot all frames to a temporary directory owned by Metview to enure cleanup
tempdirpath = tempfile.mkdtemp(dir=os.environ.get("METVIEW_TMPDIR", None))
Expand All @@ -1196,45 +1140,98 @@ def animate(*args, **kwargs):
return

files = [os.path.join(tempdirpath, f) for f in sorted(filenames)]
frame_widget.max = len(files)
frame_widget.description = "Frame (" + str(len(files)) + ") :"
play_widget.max = len(files)

if (animation_mode == True) or (animation_mode == "auto" and len(filenames) > 1):
frame_widget = widgets.IntSlider(
value=1,
min=1,
max=1,
step=1,
description="Frame:",
disabled=False,
continuous_update=True,
readout=True,
)

play_widget = widgets.Play(
value=1,
min=1,
max=1,
step=1,
interval=500,
description="Play animation",
disabled=False,
)

speed_widget = widgets.IntSlider(
value=3,
min=1,
max=20,
step=1,
description="Speed",
disabled=False,
continuous_update=True,
readout=True,
)

widgets.jslink((play_widget, "value"), (frame_widget, "value"))
play_and_speed_widget = widgets.HBox([play_widget, speed_widget])
controls = widgets.VBox([frame_widget, play_and_speed_widget])
controls.layout.visibility = "hidden"
frame_widget.layout.width = "800px"
display(controls)

frame_widget.max = len(files)
frame_widget.description = "Frame (" + str(len(files)) + ") :"
play_widget.max = len(files)

def on_frame_change(change):
plot_frame(change["new"])

def on_speed_change(change):
play_widget.interval = 1500 / change["new"]

frame_widget.observe(on_frame_change, names="value")
speed_widget.observe(on_speed_change, names="value")
controls.layout.visibility = "visible"

def plot_frame(frame_index):
im_file = open(files[frame_index - 1], "rb")
imf = im_file.read()
im_file.close()
image_widget.value = imf

def on_frame_change(change):
plot_frame(change["new"])

# everything is ready now, so plot the first frame, hide the
# 'waiting' label and reveal the plot and the frame slider
plot_frame(1)
frame_widget.observe(on_frame_change, names="value")
waitl_widget.layout.visibility = "hidden"
image_widget.layout.visibility = "visible"

def on_speed_change(change):
play_widget.interval = 1500 / change["new"]

speed_widget.observe(on_speed_change, names="value")
def plot_to_notebook_return_image(*args, **kwargs): # pragma: no cover

# everything is ready now, so hide the 'waiting' label
# and reveal the plot and the frame slider
waitl_widget.layout.visibility = "hidden"
controls.layout.visibility = "visible"
image_widget.layout.visibility = "visible"
from IPython.display import Image

f, tmp = tempfile.mkstemp(".png")
os.close(f)
base, ext = os.path.splitext(tmp)
plot.jupyter_args.update(output_name=base, output_name_first_page_number="off")
met_setoutput(png_output(plot.jupyter_args))
met_plot(*args)
image = Image(tmp)
os.unlink(tmp)
return image


# On a test system, importing IPython took approx 0.5 seconds, so to avoid that hit
# under most circumstances, we only import it when the user asks for Jupyter
# functionality. Since this occurs within a function, we need a little trickery to
# get the IPython functions into the global namespace so that the plot object can use them
def setoutput(*args, **kwargs):
if "jupyter" in args:
if "jupyter" in args: # pragma: no cover
try:
global Image
global get_ipython
IPython = __import__("IPython", globals(), locals())
Image = IPython.display.Image
import IPython

get_ipython = IPython.get_ipython
except ImportError as imperr:
print("Could not import IPython module - plotting to Jupyter will not work")
Expand All @@ -1243,12 +1240,25 @@ def setoutput(*args, **kwargs):
# test whether we're in the Jupyter environment
if get_ipython() is not None:
plot.plot_to_jupyter = True
plot.plot_widget = kwargs.get("plot_widget", True)
if "plot_widget" in kwargs:
del kwargs["plot_widget"]
plot.jupyter_args = kwargs
else:
print(
"ERROR: setoutput('jupyter') was set, but we are not in a Jupyter environment"
)
raise (Exception("Could not set output to jupyter"))

try:
global widgets
widgets = __import__("ipywidgets", globals(), locals())
except ImportError as imperr:
print(
"Could not import ipywidgets module - plotting to Jupyter will not work"
)
raise imperr

else:
plot.plot_to_jupyter = False
met_setoutput(*args)
Empty file added tests/__init__.py
Empty file.
Loading

0 comments on commit 7bdc47d

Please sign in to comment.