Skip to content

Commit

Permalink
Context manager to create and destroy C sessions
Browse files Browse the repository at this point in the history
No longer call create_session and destroy_session directly.
Fixes #24
  • Loading branch information
leouieda committed Jul 14, 2017
1 parent 8440237 commit 5f6536e
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 49 deletions.
13 changes: 11 additions & 2 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ Low-level wrappers for the GMT C API
The GMT C API is accessed using ctypes_. The ``gmt.clib`` module offers
functions and classes that wrap the C API with a pythonic interface.

Most interactions with the C API are done through the
:func:`gmt.clib.call_module` function.
Functions
+++++++++

.. autosummary::
:toctree: api/
Expand All @@ -48,5 +48,14 @@ Most interactions with the C API are done through the
gmt.clib.destroy_session
gmt.clib.load_libgmt

Classes
+++++++

.. autosummary::
:toctree: api/
:template: function.rst

gmt.clib.GMTSession


.. _ctypes: https://docs.python.org/3/library/ctypes.html
3 changes: 2 additions & 1 deletion gmt/clib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""
Low-level wrappers for the GMT C API using ctypes
"""
from .core import load_libgmt, create_session, destroy_session, call_module
from .core import load_libgmt, create_session, destroy_session, call_module, \
GMTSession
92 changes: 66 additions & 26 deletions gmt/clib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,46 @@ def load_libgmt():
return libgmt


def call_module(session, module, args):
"""
Call a GMT module with the given arguments.
Makes a call to ``GMT_Call_Module`` from the C API using mode
``GMT_MODULE_CMD`` (arguments passed as a single string).
Most interactions with the C API are done through this function.
Parameters
----------
session : ctypes.c_void_p
A ctypes void pointer to a GMT session created by
:func:`gmt.clib.GMTSession`.
module : str
Module name (``'pscoast'``, ``'psbasemap'``, etc).
args : str
String with the command line arguments that will be passed to the
module (for example, ``'-R0/5/0/10 -JM'``).
"""
mode = constants.GMT_MODULE_CMD
libgmt = load_libgmt()
c_call_module = libgmt.GMT_Call_Module
c_call_module.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int,
ctypes.c_void_p]
c_call_module.restype = ctypes.c_int
status = c_call_module(session, module.encode(), mode, args.encode())
assert status is not None, 'Failed returning None.'
assert status == 0, 'Failed with status code {}.'.format(status)


def create_session():
"""
Create the ``GMTAPI_CTRL`` struct required by the GMT C API functions.
.. warning::
Best not used directly. Use :class:`gmt.clib.GMTSession` instead.
It is a C void pointer containing the current session information and
cannot be accessed directly.
Expand Down Expand Up @@ -64,6 +100,10 @@ def destroy_session(session):
"""
Terminate and free the memory of a registered ``GMTAPI_CTRL`` session.
.. warning::
Best not used directly. Use :class:`gmt.clib.GMTSession` instead.
The session is created and consumed by the C API modules and needs to be
freed before creating a new. Otherwise, some of the configuration files
might be left behind and can influence subsequent API calls.
Expand All @@ -83,36 +123,36 @@ def destroy_session(session):
assert status == 0, 'Failed with status code {}.'.format(status)


def call_module(module, args):
class GMTSession(): # pylint: disable=too-few-public-methods
"""
Call a GMT module with the given arguments.
Context manager to create a GMT C API session and destroy it.
Makes a call to ``GMT_Call_Module`` from the C API using mode
``GMT_MODULE_CMD`` (arguments passed as a single string).
Needs to be used when wrapping a GMT module into a function.
Most interactions with the C API are done through this function.
If creating GMT data structures to communicate data, put that code inside
this context manager and reuse the same session.
Creates a new C API session (:func:`gmt.clib.create_session`) to pass to
``GMT_Call_Module`` and destroys it (:func:`gmt.clib.destroy_session`)
after it is used. This is what the command-line interface of GMT does.
Examples
--------
Parameters
----------
module : str
Module name (``'pscoast'``, ``'psbasemap'``, etc).
args : str
String with the command line arguments that will be passed to the
module (for example, ``'-R0/5/0/10 -JM'``).
>>> with GMTSession() as session:
... call_module(session, 'figure', 'my-figure')
"""
mode = constants.GMT_MODULE_CMD
libgmt = load_libgmt()
c_call_module = libgmt.GMT_Call_Module
c_call_module.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int,
ctypes.c_void_p]
c_call_module.restype = ctypes.c_int
session = create_session()
status = c_call_module(session, module.encode(), mode, args.encode())
destroy_session(session)
assert status is not None, 'Failed returning None.'
assert status == 0, 'Failed with status code {}.'.format(status)

def __init__(self):
self.session_id = None

def __enter__(self):
"""
Start the GMT session and keep the session argument.
"""
self.session_id = create_session()
return self.session_id

def __exit__(self, exc_type, exc_value, traceback):
"""
Destroy the session when exiting the context.
"""
destroy_session(self.session_id)
self.session_id = None
14 changes: 9 additions & 5 deletions gmt/ps_modules.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Function wrapper for the ps* modules.
"""
from .clib import call_module
from .clib import call_module, GMTSession
from .utils import build_arg_string
from .decorators import fmt_docstring, use_alias, kwargs_to_strings

Expand Down Expand Up @@ -68,7 +68,8 @@ def pscoast(**kwargs):
Draw shorelines [Default is no shorelines]. Append pen attributes.
"""
call_module('pscoast', build_arg_string(kwargs))
with GMTSession() as session:
call_module(session, 'pscoast', build_arg_string(kwargs))


@fmt_docstring
Expand Down Expand Up @@ -126,7 +127,8 @@ def psxy(data, **kwargs):
"""
assert isinstance(data, str), 'Only accepts file names for now.'
arg_str = ' '.join([data, build_arg_string(kwargs)])
call_module('psxy', arg_str)
with GMTSession() as session:
call_module(session, 'psxy', arg_str)


@fmt_docstring
Expand Down Expand Up @@ -174,7 +176,8 @@ def psbasemap(**kwargs):
"At least one of B, L, or T must be specified."
if 'D' in kwargs:
assert 'F' in kwargs, "Option D requires F to be specified as well."
call_module('psbasemap', build_arg_string(kwargs))
with GMTSession() as session:
call_module(session, 'psbasemap', build_arg_string(kwargs))


@fmt_docstring
Expand Down Expand Up @@ -235,4 +238,5 @@ def psconvert(**kwargs):
the *F* option.
"""
call_module('psconvert', build_arg_string(kwargs))
with GMTSession() as session:
call_module(session, 'psconvert', build_arg_string(kwargs))
11 changes: 7 additions & 4 deletions gmt/session_management.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Session management modules: begin, end, figure, etc
"""
from . import clib
from .clib import call_module, GMTSession


def begin():
Expand All @@ -14,7 +14,8 @@ def begin():
"""
prefix = 'gmt-python-session'
clib.call_module('begin', prefix)
with GMTSession() as session:
call_module(session, 'begin', prefix)


def end():
Expand All @@ -27,7 +28,8 @@ def end():
``gmt.begin``), and bring the figures to the working directory.
"""
clib.call_module('end', '')
with GMTSession() as session:
call_module(session, 'end', '')


def figure():
Expand All @@ -45,4 +47,5 @@ def figure():
prefix = 'gmt-python-figure'
# Passing format '-' tells gmt.end to not produce any files.
fmt = '-'
clib.call_module('figure', '{} {}'.format(prefix, fmt))
with GMTSession() as session:
call_module(session, 'figure', '{} {}'.format(prefix, fmt))
7 changes: 5 additions & 2 deletions gmt/tests/test_clib.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"""
import os

from ..clib import create_session, destroy_session, call_module, load_libgmt
from ..clib import create_session, destroy_session, call_module, load_libgmt, \
GMTSession


TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
Expand All @@ -30,7 +31,9 @@ def test_call_module():
"Run a psbasemap call to see if the module works"
data_fname = os.path.join(TEST_DATA_DIR, 'points.txt')
out_fname = 'test_call_module.txt'
call_module('gmtinfo', '{} -C ->{}'.format(data_fname, out_fname))
with GMTSession() as session:
call_module(session, 'gmtinfo',
'{} -C ->{}'.format(data_fname, out_fname))
assert os.path.exists(out_fname)
with open(out_fname) as out_file:
output = out_file.read().strip().replace('\t', ' ')
Expand Down
10 changes: 5 additions & 5 deletions gmt/tests/test_psconvert.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
"""
import os

from .. import clib, figure, psconvert
from .. import figure, psconvert, psbasemap


def test_psconvert():
"""
psconvert creates a figure in the current directory.
"""
figure()
clib.call_module('psbasemap', '-R10/70/-3/8 -JX4i/3i -Ba -P')
psbasemap(R='10/70/-3/8', J='X4i/3i', B='a', P=True)
prefix = 'test_psconvert'
psconvert(F=prefix, T='f', A=True, P=True)
fname = prefix + '.pdf'
Expand All @@ -24,7 +24,7 @@ def test_psconvert_twice():
Call psconvert twice to get two figures.
"""
figure()
clib.call_module('psbasemap', '-R10/70/-3/8 -JX4i/3i -Ba -P')
psbasemap(R='10/70/-3/8', J='X4i/3i', B='a', P=True)
prefix = 'test_psconvert_twice'
# Make a PDF
psconvert(F=prefix, T='f')
Expand All @@ -43,7 +43,7 @@ def test_psconvert_int_options():
psconvert handles integer options well.
"""
figure()
clib.call_module('psbasemap', '-R10/70/-3/8 -JX4i/3i -Ba -P')
psbasemap(R='10/70/-3/8', J='X4i/3i', B='a', P=True)
prefix = 'test_psconvert_int_options'
psconvert(F=prefix, E=100, T='g', I=True)
assert os.path.exists(prefix + '.png')
Expand All @@ -55,7 +55,7 @@ def test_psconvert_aliases():
Use the aliases to make sure they work.
"""
figure()
clib.call_module('psbasemap', '-R10/70/-3/8 -JX4i/3i -Ba -P')
psbasemap(R='10/70/-3/8', J='X4i/3i', B='a', P=True)
prefix = 'test_psconvert_aliases'
psconvert(prefix=prefix, fmt='g', crop=True, portrait=True, dpi=100)
fname = prefix + '.png'
Expand Down
7 changes: 3 additions & 4 deletions gmt/tests/test_session_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
"""
import os

from .. import figure
from .. import figure, psbasemap
from ..session_management import begin, end
from ..clib import call_module


def test_begin_end():
Expand All @@ -15,7 +14,7 @@ def test_begin_end():
"""
end() # Kill the global session
begin()
call_module('psbasemap', '-R10/70/-3/8 -JX4i/3i -Ba -P')
psbasemap(R='10/70/-3/8', J='X4i/3i', B='a', P=True)
end()
begin() # Restart the global session
assert os.path.exists('gmt-python-session.pdf')
Expand All @@ -31,7 +30,7 @@ def test_session_figure():
end() # Kill the global session
begin()
figure()
call_module('psbasemap', '-R10/70/-3/8 -JX4i/3i -Ba -P')
psbasemap(R='10/70/-3/8', J='X4i/3i', B='a', P=True)
end()
begin() # Restart the global session
assert not os.path.exists('gmt-python-figure.pdf')

0 comments on commit 5f6536e

Please sign in to comment.