diff --git a/.stickler.yml b/.stickler.yml index 15e2b3172e5..67d95952ace 100644 --- a/.stickler.yml +++ b/.stickler.yml @@ -1,6 +1,7 @@ linters: flake8: enable: true + ignore: F401,E226,I101,I100,E741 shellcheck: shell: bash csslint: diff --git a/gmt/clib/__init__.py b/gmt/clib/__init__.py index 72a79a7f9c2..00b7f4c3ac6 100644 --- a/gmt/clib/__init__.py +++ b/gmt/clib/__init__.py @@ -1,4 +1,4 @@ """ Low-level wrappers for the GMT C API using ctypes """ -from .context_manager import LibGMT +from .core import LibGMT diff --git a/gmt/clib/context_manager.py b/gmt/clib/context_manager.py deleted file mode 100644 index fa81bc38e1c..00000000000 --- a/gmt/clib/context_manager.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Defines the LibGMT context manager that is the main interface with libgmt. -""" -from .core import load_libgmt, create_session, destroy_session, call_module, \ - get_constant - - -class LibGMT(): - """ - Load and access the GMT shared library (libgmt). - - Works as a context manager to create a GMT C API session and destroy it in - the end. - - Functions of the shared library are exposed as methods of this class. - - If creating GMT data structures to communicate data, put that code inside - this context manager and reuse the same session. - - Examples - -------- - - >>> with LibGMT() as lib: - ... lib.call_module('figure', 'my-figure') - - """ - - def __init__(self): - self._libgmt = load_libgmt() - self._session_id = None - self._session_name = 'gmt-python-session' - - def __enter__(self): - """ - Start the GMT session and keep the session argument. - """ - self._session_id = create_session(self._session_name, self._libgmt) - return self - - def __exit__(self, exc_type, exc_value, traceback): - """ - Destroy the session when exiting the context. - """ - destroy_session(self._session_id, self._libgmt) - self._session_id = None - - def get_constant(self, name): - """ - Get the value of a constant (C enum) from gmt_resources.h - - Used to set configuration values for other API calls. Wraps - ``GMT_Get_Enum``. - - Parameters - ---------- - name : str - The name of the constant (e.g., ``"GMT_SESSION_EXTERNAL"``) - - Returns - ------- - constant : int - Integer value of the constant. Do not rely on this value because it - might change. - - Raises - ------ - GMTCLibError - If the constant doesn't exist. - - """ - value = get_constant(name, self._libgmt) - return value - - def call_module(self, 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 - ---------- - 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'``). - - Raises - ------ - GMTCLibError - If the returned status code of the functions is non-zero. - - """ - call_module(self._session_id, module, args, self._libgmt) diff --git a/gmt/clib/core.py b/gmt/clib/core.py index c091c3a28d7..b4a57e2eddb 100644 --- a/gmt/clib/core.py +++ b/gmt/clib/core.py @@ -3,47 +3,25 @@ """ import ctypes -from ..exceptions import GMTCLibNotFoundError, GMTCLibError -from .utils import clib_extension, check_status_code +from ..exceptions import GMTCLibError, GMTCLibNoSessionError +from .utils import load_libgmt, check_status_code, kwargs_to_ctypes_array -def _check_libgmt(libgmt): +class LibGMT(): # pylint: disable=too-many-instance-attributes """ - Make sure that libgmt was loaded correctly. + Load and access the GMT shared library (libgmt). - Checks if it defines some common required functions. + Works as a context manager to create a GMT C API session and destroy it in + the end. The context manager feature eliminates the need for the + ``GMT_Create_Session`` and ``GMT_Destroy_Session`` functions. Thus, they + are not exposed in the Python API. If you need the void pointer to the GMT + session, use the ``current_session`` attribute. - Does nothing if everything is fine. Raises an exception if any of the - functions are missing. + Functions of the shared library are exposed as methods of this class. Most + methods MUST be used inside the context manager 'with' block. - Parameters - ---------- - libgmt : ctypes.CDLL - A shared library loaded using ctypes. - - Raises - ------ - GMTCLibError - - """ - # Check if a few of the functions we need are in the library - functions = ['Create_Session', 'Get_Enum', 'Call_Module', - 'Destroy_Session'] - for func in functions: - if not hasattr(libgmt, 'GMT_' + func): - msg = ' '.join([ - "Error loading libgmt.", - "Couldn't access function GMT_{}.".format(func), - ]) - raise GMTCLibError(msg) - - -def load_libgmt(libname='libgmt'): - """ - Find and load ``libgmt`` as a ctypes.CDLL. - - If not given the full path to the library, it must be in standard places or - by discoverable by setting the environment variable ``LD_LIBRARY_PATH``. + If creating GMT data structures to communicate data, put that code inside + this context manager to reuse the same session. Parameters ---------- @@ -51,172 +29,378 @@ def load_libgmt(libname='libgmt'): The name of the GMT shared library **without the extension**. Can be a full path to the library or just the library name. - Returns - ------- - ctypes.CDLL object - The loaded shared library. - Raises ------ GMTCLibNotFoundError If there was any problem loading the library (couldn't find it or couldn't access the functions). + GMTCLibNoSessionError + If you try to call a method outside of a 'with' block. - """ - try: - libgmt = ctypes.CDLL('.'.join([libname, clib_extension()])) - _check_libgmt(libgmt) - except OSError as err: - msg = ' '.join([ - "Couldn't find the GMT shared library '{}'.".format(libname), - "Have you tried setting the LD_LIBRARY_PATH environment variable?", - "\nOriginal error message:", - "\n\n {}".format(str(err)), - ]) - raise GMTCLibNotFoundError(msg) - return libgmt - - -def create_session(session_name, libgmt): - """ - Create the ``GMTAPI_CTRL`` struct required by the GMT C API functions. - - It is a C void pointer containing the current session information and - cannot be accessed directly. + Examples + -------- - Remember to terminate the current session using - :func:`gmt.clib.LibGMT._destroy_session` before creating a new one. + >>> with LibGMT() as lib: + ... lib.call_module('figure', 'my-figure') - Parameters - ---------- - session_name : str - A name for this session. Doesn't really affect the outcome. - libgmt : ctypes.CDLL - The ``ctypes.CDLL`` instance for the libgmt shared library. - - Returns - ------- - api_pointer : C void pointer (returned by ctypes as an integer) - Used by GMT C API functions. - - """ - c_create_session = libgmt.GMT_Create_Session - c_create_session.argtypes = [ctypes.c_char_p, ctypes.c_uint, - ctypes.c_uint, ctypes.c_void_p] - c_create_session.restype = ctypes.c_void_p - # None is passed in place of the print function pointer. It becomes the - # NULL pointer when passed to C, prompting the C API to use the default - # print function. - print_func = None - padding = get_constant('GMT_PAD_DEFAULT', libgmt) - session_type = get_constant('GMT_SESSION_EXTERNAL', libgmt) - session = c_create_session(session_name.encode(), padding, session_type, - print_func) - - if session is None: - raise GMTCLibError("Failed to create a GMT API void pointer.") - - return session - - -def destroy_session(session, libgmt): """ - Terminate and free the memory of a registered ``GMTAPI_CTRL`` session. - 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. - - Parameters - ---------- - session : C void pointer (returned by ctypes as an integer) - The active session object produced by - :func:`gmt.clib.core.create_session`. - libgmt : ctypes.CDLL - The ``ctypes.CDLL`` instance for the libgmt shared library. - - """ - c_destroy_session = libgmt.GMT_Destroy_Session - c_destroy_session.argtypes = [ctypes.c_void_p] - c_destroy_session.restype = ctypes.c_int - - status = c_destroy_session(session) - check_status_code(status, 'GMT_Destroy_Session') - - -def get_constant(name, libgmt): - """ - Get the value of a constant (C enum) from gmt_resources.h - - Used to set configuration values for other API calls. Wraps - ``GMT_Get_Enum``. - - Parameters - ---------- - name : str - The name of the constant (e.g., ``"GMT_SESSION_EXTERNAL"``) - libgmt : ctypes.CDLL - The ``ctypes.CDLL`` instance for the libgmt shared library. - - Returns - ------- - constant : int - Integer value of the constant. Do not rely on this value because it - might change. - - Raises - ------ - GMTCLibError - If the constant doesn't exist. - - """ - c_get_enum = libgmt.GMT_Get_Enum - c_get_enum.argtypes = [ctypes.c_char_p] - c_get_enum.restype = ctypes.c_int - - value = c_get_enum(name.encode()) - - if value is None or value == -99999: - raise GMTCLibError( - "Constant '{}' doesn't exits in libgmt.".format(name)) - - return value - - -def call_module(session, module, args, libgmt): - """ - 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 : C void pointer (returned by ctypes as an integer) - The active session object produced by - :func:`gmt.clib.core.create_session`. - 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'``). - libgmt : ctypes.CDLL - The ``ctypes.CDLL`` instance for the libgmt shared library. - - Raises - ------ - GMTCLibError - If the returned status code of the functions is non-zero. - - """ - 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 - - mode = get_constant('GMT_MODULE_CMD', libgmt) - status = c_call_module(session, module.encode(), mode, - args.encode()) - check_status_code(status, 'GMT_Call_Module') + data_families = [ + 'GMT_IS_DATASET', + 'GMT_IS_GRID', + 'GMT_IS_PALETTE', + 'GMT_IS_MATRIX', + 'GMT_IS_VECTOR', + ] + + data_vias = [ + 'GMT_VIA_MATRIX', + 'GMT_VIA_VECTOR', + ] + + data_geometries = [ + 'GMT_IS_NONE', + 'GMT_IS_POINT', + 'GMT_IS_LINE', + 'GMT_IS_POLYGON', + 'GMT_IS_PLP', + 'GMT_IS_SURFACE', + ] + + data_modes = [ + 'GMT_CONTAINER_ONLY', + 'GMT_OUTPUT', + ] + + def __init__(self, libname='libgmt'): + self._session_id = None + self._session_name = 'gmt-python-session' + self._libgmt = None + self._c_get_enum = None + self._c_create_session = None + self._c_destroy_session = None + self._c_call_module = None + self._c_create_data = None + self._bind_clib_functions(libname) + + @property + def current_session(self): + """ + The C void pointer for the current open GMT session. + + Raises + ------ + GMTCLibNoSessionError + If trying to access without a currently open GMT session (i.e., + outside of the context manager). + + """ + if self._session_id is None: + raise GMTCLibNoSessionError(' '.join([ + "No currently open session.", + "Call methods only inside a 'with' block."])) + return self._session_id + + @current_session.setter + def current_session(self, session): + """ + Set the session void pointer. + """ + self._session_id = session + + def _bind_clib_functions(self, libname): + """ + Loads libgmt and binds the library functions to class attributes. + + Sets the argument and return types for the functions. + """ + self._libgmt = load_libgmt(libname) + + self._c_create_session = self._libgmt.GMT_Create_Session + self._c_create_session.argtypes = [ctypes.c_char_p, ctypes.c_uint, + ctypes.c_uint, ctypes.c_void_p] + self._c_create_session.restype = ctypes.c_void_p + + self._c_destroy_session = self._libgmt.GMT_Destroy_Session + self._c_destroy_session.argtypes = [ctypes.c_void_p] + self._c_destroy_session.restype = ctypes.c_int + + self._c_get_enum = self._libgmt.GMT_Get_Enum + self._c_get_enum.argtypes = [ctypes.c_char_p] + self._c_get_enum.restype = ctypes.c_int + + self._c_call_module = self._libgmt.GMT_Call_Module + self._c_call_module.argtypes = [ctypes.c_void_p, ctypes.c_char_p, + ctypes.c_int, ctypes.c_void_p] + self._c_call_module.restype = ctypes.c_int + + self._c_create_data = self._libgmt.GMT_Create_Data + self._c_create_data.argtypes = [ + ctypes.c_void_p, # API + ctypes.c_uint, # family + ctypes.c_uint, # geometry + ctypes.c_uint, # mode + ctypes.POINTER(ctypes.c_uint64), # dim + ctypes.POINTER(ctypes.c_double), # range + ctypes.POINTER(ctypes.c_double), # inc + ctypes.c_uint, # registration + ctypes.c_int, # pad + ctypes.c_void_p, # data + ] + self._c_create_data.restype = ctypes.c_void_p + + def __enter__(self): + """ + Start the GMT session and keep the session argument. + """ + self.current_session = self.create_session(self._session_name) + return self + + def __exit__(self, exc_type, exc_value, traceback): + """ + Destroy the session when exiting the context. + """ + self.destroy_session(self.current_session) + self.current_session = None + + def create_session(self, session_name): + """ + Create the ``GMTAPI_CTRL`` struct required by the GMT C API functions. + + It is a C void pointer containing the current session information and + cannot be accessed directly. + + Remember to terminate the current session using + :func:`gmt.clib.LibGMT.destroy_session` before creating a new one. + + Parameters + ---------- + session_name : str + A name for this session. Doesn't really affect the outcome. + + Returns + ------- + api_pointer : C void pointer (returned by ctypes as an integer) + Used by GMT C API functions. + + """ + # None is passed in place of the print function pointer. It becomes the + # NULL pointer when passed to C, prompting the C API to use the default + # print function. + print_func = None + padding = self.get_constant('GMT_PAD_DEFAULT') + session_type = self.get_constant('GMT_SESSION_EXTERNAL') + session = self._c_create_session(session_name.encode(), padding, + session_type, print_func) + + if session is None: + raise GMTCLibError("Failed to create a GMT API void pointer.") + + return session + + def destroy_session(self, session): + """ + Terminate and free the memory of a registered ``GMTAPI_CTRL`` session. + + 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. + + Parameters + ---------- + session : C void pointer (returned by ctypes as an integer) + The active session object produced by + :func:`gmt.clib.LibGMT.create_session`. + libgmt : ctypes.CDLL + The ``ctypes.CDLL`` instance for the libgmt shared library. + + """ + status = self._c_destroy_session(session) + check_status_code(status, 'GMT_Destroy_Session') + + def get_constant(self, name): + """ + Get the value of a constant (C enum) from gmt_resources.h + + Used to set configuration values for other API calls. Wraps + ``GMT_Get_Enum``. + + Parameters + ---------- + name : str + The name of the constant (e.g., ``"GMT_SESSION_EXTERNAL"``) + + Returns + ------- + constant : int + Integer value of the constant. Do not rely on this value because it + might change. + + Raises + ------ + GMTCLibError + If the constant doesn't exist. + + """ + value = self._c_get_enum(name.encode()) + + if value is None or value == -99999: + raise GMTCLibError( + "Constant '{}' doesn't exits in libgmt.".format(name)) + + return value + + def call_module(self, 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 + ---------- + 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'``). + + Raises + ------ + GMTCLibError + If the returned status code of the functions is non-zero. + + """ + mode = self.get_constant('GMT_MODULE_CMD') + status = self._c_call_module(self.current_session, module.encode(), + mode, args.encode()) + check_status_code(status, 'GMT_Call_Module') + + def create_data(self, family, geometry, mode, **kwargs): + """ + Create an empty GMT data container. + + Parameters + ---------- + family : str + A valid GMT data family name (e.g., ``'GMT_IS_DATASET'``). See the + ``data_families`` attribute for valid names. + geometry : str + A valid GMT data geometry name (e.g., ``'GMT_IS_POINT'``). See the + ``data_geometries`` attribute for valid names. + mode : str + A valid GMT data mode (e.g., ``'GMT_OUTPUT'``). See the + ``data_modes`` attribute for valid names. + dim : list of 4 integers + The dimensions of the dataset. See the documentation for the GMT C + API function ``GMT_Create_Data`` (``src/gmt_api.c``) for the full + range of options regarding 'dim'. If ``None``, will pass in the + NULL pointer. + ranges : list of 4 floats + The dataset extent. Also a bit of a complicated argument. See the C + function documentation. It's called ``range`` in the C function but + it would conflict with the Python built-in ``range`` function. + inc : list of 2 floats + The increments between points of the dataset. See the C function + documentation. + registration : int + The node registration (what the coordinates mean). Also a very + complex argument. See the C function documentation. Defaults to + ``GMT_GRID_NODE_REG``. + pad : int + The grid padding. Defaults to ``GMT_PAD_DEFAULT``. + + Returns + ------- + data_ptr : int + A ctypes pointer (an integer) to the allocated ``GMT_Dataset`` + object. + + Raises + ------ + GMTCLibError + In case of invalid inputs or data_ptr being NULL. + + """ + # Parse and check input arguments + family_int = self._parse_data_family(family) + if mode not in self.data_modes: + raise GMTCLibError("Invalid data creation mode '{}'.".format(mode)) + if geometry not in self.data_geometries: + raise GMTCLibError("Invalid data geometry '{}'.".format(geometry)) + + # Convert dim, ranges, and inc to ctypes arrays if given + dim = kwargs_to_ctypes_array('dim', kwargs, ctypes.c_uint64*4) + ranges = kwargs_to_ctypes_array('ranges', kwargs, ctypes.c_double*4) + inc = kwargs_to_ctypes_array('inc', kwargs, ctypes.c_double*2) + + # Use the GMT defaults if no value is given + registration = kwargs.get('registration', + self.get_constant('GMT_GRID_NODE_REG')) + pad = kwargs.get('pad', self.get_constant('GMT_PAD_DEFAULT')) + + data_ptr = self._c_create_data( + self.current_session, + family_int, + self.get_constant(geometry), + self.get_constant(mode), + dim, + ranges, + inc, + registration, + pad, + None, # NULL pointer for existing data + ) + + if data_ptr is None: + raise GMTCLibError("Failed to create an empty GMT data pointer.") + + return data_ptr + + def _parse_data_family(self, family): + """ + Parse the data family string into an integer number. + + Valid family names are: GMT_IS_DATASET, GMT_IS_GRID, GMT_IS_PALETTE, + GMT_IS_TEXTSET, GMT_IS_MATRIX, and GMT_IS_VECTOR. + + Optionally append a "via" argument to a family name (separated by + ``|``): GMT_VIA_MATRIX or GMT_VIA_VECTOR. + + Parameters + ---------- + family : str + A GMT data family name. + + Returns + ------- + family_value : int + The GMT constant corresponding to the family. + + Raises + ------ + GMTCLibError + If the family name is invalid or there are more than 2 components + to the name. + + """ + parts = family.split('|') + if len(parts) > 2: + raise GMTCLibError( + "Too many sections in family (>2): '{}'".format(family)) + family_name = parts[0] + if family_name not in self.data_families: + raise GMTCLibError( + "Invalid data family '{}'.".format(family_name)) + family_value = self.get_constant(family_name) + if len(parts) == 2: + via_name = parts[1] + if via_name not in self.data_vias: + raise GMTCLibError( + "Invalid data family (via) '{}'.".format(via_name)) + via_value = self.get_constant(via_name) + else: + via_value = 0 + return family_value + via_value diff --git a/gmt/clib/io.py b/gmt/clib/io.py deleted file mode 100644 index 1f26e26e28a..00000000000 --- a/gmt/clib/io.py +++ /dev/null @@ -1,218 +0,0 @@ -""" -Wrappers for creating and accessing GMT data containers. -""" -import ctypes - -from ..exceptions import GMTCLibError -from .core import get_constant - - -DATA_FAMILIES = [ - 'GMT_IS_DATASET', - 'GMT_IS_GRID', - 'GMT_IS_PALETTE', - 'GMT_IS_MATRIX', - 'GMT_IS_VECTOR', -] -DATA_VIAS = [ - 'GMT_VIA_MATRIX', - 'GMT_VIA_VECTOR', -] -DATA_GEOMETRIES = [ - 'GMT_IS_NONE', - 'GMT_IS_POINT', - 'GMT_IS_LINE', - 'GMT_IS_POLYGON', - 'GMT_IS_PLP', - 'GMT_IS_SURFACE', -] -DATA_MODES = [ - 'GMT_CONTAINER_ONLY', - 'GMT_OUTPUT', -] - - -def create_data(libgmt, session, family, geometry, mode, **kwargs): - """ - Create an empty GMT data container. - - Parameters - ---------- - libgmt : ctypes.CDLL - The ``ctypes.CDLL`` instance for the libgmt shared library. - session : C void pointer (returned by ctypes as an integer) - The active session object produced by - :func:`gmt.clib.core.create_session`. - family : str - A valid GMT data family name (e.g., ``'GMT_IS_DATASET'``). See the - ``DATA_FAMILIES`` variable for valid names. - geometry : str - A valid GMT data geometry name (e.g., ``'GMT_IS_POINT'``). See the - ``DATA_GEOMETRIES`` variable for valid names. - mode : str - A valid GMT data mode (e.g., ``'GMT_OUTPUT'``). See the ``DATA_MODES`` - variable for valid names. - dim : list of 4 integers - The dimensions of the dataset. See the documentation for the GMT C API - function ``GMT_Create_Data`` (``src/gmt_api.c``) for the full range of - options regarding 'dim'. If ``None``, will pass in the NULL pointer. - ranges : list of 4 floats - The dataset extent. Also a bit of a complicated argument. See the C - function documentation. It's called ``range`` in the C function but it - would conflict with the Python built-in ``range`` function. - inc : list of 2 floats - The increments between points of the dataset. See the C function - documentation. - registration : int - The node registration (what the coordinates mean). Also a very complex - argument. See the C function documentation. Defaults to - ``GMT_GRID_NODE_REG``. - pad : int - The grid padding. Defaults to ``GMT_PAD_DEFAULT``. - - Returns - ------- - data_ptr : int - A ctypes pointer (an integer) to the allocated ``GMT_Dataset`` object. - - Raises - ------ - GMTCLibError - In case of invalid inputs or data_ptr being NULL. - - """ - # Parse and check input arguments - family_int = _parse_data_family(libgmt, family) - if mode not in DATA_MODES: - raise GMTCLibError("Invalid data creation mode '{}'.".format(mode)) - if geometry not in DATA_GEOMETRIES: - raise GMTCLibError("Invalid data geometry '{}'.".format(geometry)) - - # Convert dim, ranges, and inc to ctypes arrays if given - dim = _kwargs_to_ctypes_array('dim', kwargs, ctypes.c_uint64*4) - ranges = _kwargs_to_ctypes_array('ranges', kwargs, ctypes.c_double*4) - inc = _kwargs_to_ctypes_array('inc', kwargs, ctypes.c_double*2) - - # Use the GMT defaults if no value is given - registration = kwargs.get('registration', - get_constant('GMT_GRID_NODE_REG', libgmt)) - pad = kwargs.get('pad', get_constant('GMT_PAD_DEFAULT', libgmt)) - - # Get the C function and set the argument types - c_create_data = libgmt.GMT_Create_Data - c_create_data.argtypes = [ - ctypes.c_void_p, # API - ctypes.c_uint, # family - ctypes.c_uint, # geometry - ctypes.c_uint, # mode - ctypes.POINTER(ctypes.c_uint64), # dim - ctypes.POINTER(ctypes.c_double), # range - ctypes.POINTER(ctypes.c_double), # inc - ctypes.c_uint, # registration - ctypes.c_int, # pad - ctypes.c_void_p, # data - ] - c_create_data.restype = ctypes.c_void_p - - data_ptr = c_create_data( - session, - family_int, - get_constant(geometry, libgmt), - get_constant(mode, libgmt), - dim, - ranges, - inc, - registration, - pad, - None, # NULL pointer for existing data - ) - - if data_ptr is None: - raise GMTCLibError("Failed to create an empty GMT data pointer.") - - return data_ptr - - -def _kwargs_to_ctypes_array(argument, kwargs, dtype): - """ - Convert an iterable argument from kwargs into a ctypes array variable. - - If the argument is not present in kwargs, returns ``None``. - - Parameters - ---------- - argument : str - The name of the argument. - kwargs : dict - Dictionary of keyword arguments. - dtype : ctypes type - The ctypes array type (e.g., ``ctypes.c_double*4``) - - Returns - ------- - ctypes_value : ctypes array or None - - Examples - -------- - - >>> import ctypes as ct - >>> value = _kwargs_to_ctypes_array('bla', {'bla': [10, 10]}, ct.c_int*2) - >>> type(value) - - >>> b = 1 - >>> should_be_none = _kwargs_to_ctypes_array( - ... 'swallow', {'bla': 1, 'foo': [20, 30]}, ct.c_int*2) - >>> print(should_be_none) - None - - """ - if argument in kwargs: - return dtype(*kwargs[argument]) - return None - - -def _parse_data_family(libgmt, family): - """ - Parse the data family string into a GMT constant number. - - Valid family names are: GMT_IS_DATASET, GMT_IS_GRID, GMT_IS_PALETTE, - GMT_IS_TEXTSET, GMT_IS_MATRIX, and GMT_IS_VECTOR. - - Optionally append a "via" argument to a family name (separated by - ``|``): GMT_VIA_MATRIX or GMT_VIA_VECTOR. - - Parameters - ---------- - family : str - A GMT data family name. - - Returns - ------- - family_value : int - The GMT constant corresponding to the family. - - Raises - ------ - GMTCLibError - If the family name is invalid or there are more than 2 components - to the name. - - """ - parts = family.split('|') - if len(parts) > 2: - raise GMTCLibError( - "Too many sections in family (>2): '{}'".format(family)) - family_name = parts[0] - if family_name not in DATA_FAMILIES: - raise GMTCLibError( - "Invalid data family '{}'.".format(family_name)) - family_value = get_constant(family_name, libgmt) - if len(parts) == 2: - via_name = parts[1] - if via_name not in DATA_VIAS: - raise GMTCLibError( - "Invalid data family (via) '{}'.".format(via_name)) - via_value = get_constant(via_name, libgmt) - else: - via_value = 0 - return family_value + via_value diff --git a/gmt/clib/utils.py b/gmt/clib/utils.py index a1b6cec9744..0e0a6c0caad 100644 --- a/gmt/clib/utils.py +++ b/gmt/clib/utils.py @@ -2,8 +2,48 @@ Miscellaneous utilities """ import sys +import ctypes -from ..exceptions import GMTOSError, GMTCLibError +from ..exceptions import GMTOSError, GMTCLibError, GMTCLibNotFoundError + + +def load_libgmt(libname='libgmt'): + """ + Find and load ``libgmt`` as a ctypes.CDLL. + + If not given the full path to the library, it must be in standard places or + by discoverable by setting the environment variable ``LD_LIBRARY_PATH``. + + Parameters + ---------- + libname : str + The name of the GMT shared library **without the extension**. Can be a + full path to the library or just the library name. + + Returns + ------- + ctypes.CDLL object + The loaded shared library. + + Raises + ------ + GMTCLibNotFoundError + If there was any problem loading the library (couldn't find it or + couldn't access the functions). + + """ + try: + libgmt = ctypes.CDLL('.'.join([libname, clib_extension()])) + check_libgmt(libgmt) + except OSError as err: + msg = ' '.join([ + "Couldn't find the GMT shared library '{}'.".format(libname), + "Have you tried setting the LD_LIBRARY_PATH environment variable?", + "\nOriginal error message:", + "\n\n {}".format(str(err)), + ]) + raise GMTCLibNotFoundError(msg) + return libgmt def clib_extension(os_name=None): @@ -35,10 +75,42 @@ def clib_extension(os_name=None): # Darwin is OSX lib_ext = 'dylib' else: - raise GMTOSError('Unknown operating system: {}'.format(sys.platform)) + raise GMTOSError( + 'Operating system "{}" not supported.'.format(sys.platform)) return lib_ext +def check_libgmt(libgmt): + """ + Make sure that libgmt was loaded correctly. + + Checks if it defines some common required functions. + + Does nothing if everything is fine. Raises an exception if any of the + functions are missing. + + Parameters + ---------- + libgmt : ctypes.CDLL + A shared library loaded using ctypes. + + Raises + ------ + GMTCLibError + + """ + # Check if a few of the functions we need are in the library + functions = ['Create_Session', 'Get_Enum', 'Call_Module', + 'Destroy_Session'] + for func in functions: + if not hasattr(libgmt, 'GMT_' + func): + msg = ' '.join([ + "Error loading libgmt.", + "Couldn't access function GMT_{}.".format(func), + ]) + raise GMTCLibError(msg) + + def check_status_code(status, function): """ Check if the status code returned by a function is non-zero. @@ -60,3 +132,41 @@ def check_status_code(status, function): if status is None or status != 0: raise GMTCLibError( 'Failed {} with status code {}.'.format(function, status)) + + +def kwargs_to_ctypes_array(argument, kwargs, dtype): + """ + Convert an iterable argument from kwargs into a ctypes array variable. + + If the argument is not present in kwargs, returns ``None``. + + Parameters + ---------- + argument : str + The name of the argument. + kwargs : dict + Dictionary of keyword arguments. + dtype : ctypes type + The ctypes array type (e.g., ``ctypes.c_double*4``) + + Returns + ------- + ctypes_value : ctypes array or None + + Examples + -------- + + >>> import ctypes as ct + >>> value = kwargs_to_ctypes_array('bla', {'bla': [10, 10]}, ct.c_int*2) + >>> type(value) + + >>> b = 1 + >>> should_be_none = kwargs_to_ctypes_array( + ... 'swallow', {'bla': 1, 'foo': [20, 30]}, ct.c_int*2) + >>> print(should_be_none) + None + + """ + if argument in kwargs: + return dtype(*kwargs[argument]) + return None diff --git a/gmt/exceptions.py b/gmt/exceptions.py index be6fd533e4b..7c45e3e8eb8 100644 --- a/gmt/exceptions.py +++ b/gmt/exceptions.py @@ -17,15 +17,22 @@ class GMTOSError(GMTError): pass -class GMTCLibNotFoundError(GMTError): +class GMTCLibError(GMTError): + """ + Error encountered when running a function from the GMT shared library. + """ + pass + + +class GMTCLibNotFoundError(GMTCLibError): """ Could not find the GMT shared library. """ pass -class GMTCLibError(GMTError): +class GMTCLibNoSessionError(GMTCLibError): """ - Error encountered when running a function from the GMT shared library. + Tried to access GMT API without a currently open GMT session. """ pass diff --git a/gmt/tests/test_clib.py b/gmt/tests/test_clib.py index 658a1e766ed..c4119c37794 100644 --- a/gmt/tests/test_clib.py +++ b/gmt/tests/test_clib.py @@ -6,11 +6,10 @@ import pytest -from ..clib.core import load_libgmt, _check_libgmt, create_session, \ - destroy_session, call_module, get_constant -from ..clib.context_manager import LibGMT -from ..clib.utils import clib_extension -from ..exceptions import GMTCLibError, GMTOSError, GMTCLibNotFoundError +from ..clib.core import LibGMT +from ..clib.utils import clib_extension, load_libgmt, check_libgmt +from ..exceptions import GMTCLibError, GMTOSError, GMTCLibNotFoundError, \ + GMTCLibNoSessionError TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') @@ -30,7 +29,7 @@ def test_load_libgmt_fail(): def test_check_libgmt(): "Make sure check_libgmt fails when given a bogus library" with pytest.raises(GMTCLibError): - _check_libgmt(dict()) + check_libgmt(dict()) def test_clib_extension(): @@ -44,42 +43,39 @@ def test_clib_extension(): def test_constant(): "Test that I can get correct constants from the C lib" - lib = load_libgmt() - assert get_constant('GMT_SESSION_EXTERNAL', lib) != -99999 - assert get_constant('GMT_MODULE_CMD', lib) != -99999 - assert get_constant('GMT_PAD_DEFAULT', lib) != -99999 + lib = LibGMT() + assert lib.get_constant('GMT_SESSION_EXTERNAL') != -99999 + assert lib.get_constant('GMT_MODULE_CMD') != -99999 + assert lib.get_constant('GMT_PAD_DEFAULT') != -99999 with pytest.raises(GMTCLibError): - get_constant('A_WHOLE_LOT_OF_JUNK', lib) + lib.get_constant('A_WHOLE_LOT_OF_JUNK') def test_clib_session_management(): "Test that create and destroy session are called without errors" - lib = load_libgmt() - session1 = create_session(session_name='test_session1', libgmt=lib) + lib = LibGMT() + session1 = lib.create_session(session_name='test_session1') assert session1 is not None - session2 = create_session(session_name='test_session2', libgmt=lib) + session2 = lib.create_session(session_name='test_session2') assert session2 is not None assert session2 != session1 - destroy_session(session1, libgmt=lib) - destroy_session(session2, libgmt=lib) + lib.destroy_session(session1) + lib.destroy_session(session2) def test_destroy_session_fails(): "Fail to destroy session when given bad input" - lib = load_libgmt() + lib = LibGMT() with pytest.raises(GMTCLibError): - destroy_session(None, lib) + lib.destroy_session(None) def test_call_module(): "Run a command to see if call_module works" data_fname = os.path.join(TEST_DATA_DIR, 'points.txt') out_fname = 'test_call_module.txt' - lib = load_libgmt() - session = create_session('test_call_module', lib) - call_module(session, 'gmtinfo', '{} -C ->{}'.format(data_fname, out_fname), - lib) - destroy_session(session, lib) + with LibGMT() as lib: + lib.call_module('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', ' ') @@ -89,22 +85,121 @@ def test_call_module(): def test_call_module_fails(): "Fails when given bad input" - lib = load_libgmt() - session = create_session('test_call_module_fails', lib) - with pytest.raises(GMTCLibError): - call_module(session, 'meh', '', lib) - destroy_session(session, lib) + with LibGMT() as lib: + with pytest.raises(GMTCLibError): + lib.call_module('meh', '') -def test_call_module_no_session(): +def test_method_no_session(): "Fails when not in a session" - lib = load_libgmt() - with pytest.raises(GMTCLibError): - call_module(None, 'gmtdefaults', '', lib) + # Create an instance of LibGMT without "with" so no session is created. + lib = LibGMT() + with pytest.raises(GMTCLibNoSessionError): + lib.call_module('gmtdefaults', '') + with pytest.raises(GMTCLibNoSessionError): + lib.current_session # pylint: disable=pointless-statement + + +def test_parse_data_family_single(): + "Parsing a single family argument correctly." + lib = LibGMT() + for family in lib.data_families: + assert lib._parse_data_family(family) == lib.get_constant(family) + + +def test_parse_data_family_via(): + "Parsing a composite family argument (separated by |) correctly." + lib = LibGMT() + test_cases = ((family, via) + for family in lib.data_families + for via in lib.data_vias) + for family, via in test_cases: + composite = '|'.join([family, via]) + expected = lib.get_constant(family) + lib.get_constant(via) + assert lib._parse_data_family(composite) == expected + + +def test_parse_data_family_fails(): + "Check if the function fails when given bad input" + lib = LibGMT() + test_cases = [ + 'SOME_random_STRING', + 'GMT_IS_DATASET|GMT_VIA_MATRIX|GMT_VIA_VECTOR', + 'GMT_IS_DATASET|NOT_A_PROPER_VIA', + 'NOT_A_PROPER_FAMILY|GMT_VIA_MATRIX', + 'NOT_A_PROPER_FAMILY|ALSO_INVALID', + ] + for test_case in test_cases: + with pytest.raises(GMTCLibError): + lib._parse_data_family(test_case) + + +def test_create_data_dataset(): + "Run the function to make sure it doesn't fail badly." + with LibGMT() as lib: + # Dataset from vectors + data_vector = lib.create_data( + family='GMT_IS_DATASET|GMT_VIA_VECTOR', + geometry='GMT_IS_POINT', + mode='GMT_CONTAINER_ONLY', + dim=[10, 20, 1, 0], # columns, rows, layers, dtype + ) + # Dataset from matrices + data_matrix = lib.create_data( + family='GMT_IS_DATASET|GMT_VIA_MATRIX', + geometry='GMT_IS_POINT', + mode='GMT_CONTAINER_ONLY', + dim=[10, 20, 1, 0], + ) + assert data_vector != data_matrix + + +def test_create_data_grid_dim(): + "Run the function to make sure it doesn't fail badly." + with LibGMT() as lib: + # Grids from matrices using dim + lib.create_data( + family='GMT_IS_GRID|GMT_VIA_MATRIX', + geometry='GMT_IS_SURFACE', + mode='GMT_CONTAINER_ONLY', + dim=[10, 20, 1, 0], + ) -def test_context_manager(): - "Test the LibGMT context manager" +def test_create_data_grid_range(): + "Run the function to make sure it doesn't fail badly." + with LibGMT() as lib: + # Grids from matrices using range and int + lib.create_data( + family='GMT_IS_GRID|GMT_VIA_MATRIX', + geometry='GMT_IS_SURFACE', + mode='GMT_CONTAINER_ONLY', + dim=[0, 0, 1, 0], + ranges=[150., 250., -20., 20.], + inc=[0.1, 0.2], + ) + + +def test_create_data_fails(): + "Test for failures on bad input" with LibGMT() as lib: - lib.get_constant('GMT_SESSION_EXTERNAL') - lib.call_module('psbasemap', '-R0/1/0/1 -JX6i -Bafg') + # Passing in invalid mode + with pytest.raises(GMTCLibError): + lib.create_data( + family='GMT_IS_DATASET', + geometry='GMT_IS_SURFACE', + mode='Not_a_valid_mode', + dim=[0, 0, 1, 0], + ranges=[150., 250., -20., 20.], + inc=[0.1, 0.2], + ) + # Passing in invalid geometry + with pytest.raises(GMTCLibError): + lib.create_data( + family='GMT_IS_GRID', + geometry='Not_a_valid_geometry', + mode='GMT_CONTAINER_ONLY', + dim=[0, 0, 1, 0], + ranges=[150., 250., -20., 20.], + inc=[0.1, 0.2], + ) diff --git a/gmt/tests/test_clib_io.py b/gmt/tests/test_clib_io.py deleted file mode 100644 index 785169f03f0..00000000000 --- a/gmt/tests/test_clib_io.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -Test the creation and manipulation of GMT data containers. -""" -import pytest - -from ..exceptions import GMTCLibError -from ..clib.core import load_libgmt, create_session, destroy_session, \ - get_constant -from ..clib.io import create_data, _parse_data_family, \ - DATA_FAMILIES, DATA_VIAS - - -def test_parse_data_family_single(): - "Parsing a single family argument correctly." - lib = load_libgmt() - for family in DATA_FAMILIES: - assert _parse_data_family(lib, family) == get_constant(family, lib) - - -def test_parse_data_family_via(): - "Parsing a composite family argument (separated by |) correctly." - lib = load_libgmt() - test_cases = ((family, via) - for family in DATA_FAMILIES - for via in DATA_VIAS) - for family, via in test_cases: - composite = '|'.join([family, via]) - expected = get_constant(family, lib) + get_constant(via, lib) - assert _parse_data_family(lib, composite) == expected - - -def test_parse_data_family_fails(): - "Check if the function fails when given bad input" - lib = load_libgmt() - test_cases = [ - 'SOME_random_STRING', - 'GMT_IS_DATASET|GMT_VIA_MATRIX|GMT_VIA_VECTOR', - 'GMT_IS_DATASET|NOT_A_PROPER_VIA', - 'NOT_A_PROPER_FAMILY|GMT_VIA_MATRIX', - 'NOT_A_PROPER_FAMILY|ALSO_INVALID', - ] - for test_case in test_cases: - with pytest.raises(GMTCLibError): - _parse_data_family(lib, test_case) - - -def test_create_data_dataset(): - "Run the function to make sure it doesn't fail badly." - lib = load_libgmt() - session = create_session('test_create_data', lib) - # Dataset from vectors - data_vector = create_data( - libgmt=lib, - session=session, - family='GMT_IS_DATASET|GMT_VIA_VECTOR', - geometry='GMT_IS_POINT', - mode='GMT_CONTAINER_ONLY', - dim=[10, 20, 1, 0], # columns, rows, layers, dtype - ) - # Dataset from matrices - data_matrix = create_data( - libgmt=lib, - session=session, - family='GMT_IS_DATASET|GMT_VIA_MATRIX', - geometry='GMT_IS_POINT', - mode='GMT_CONTAINER_ONLY', - dim=[10, 20, 1, 0], - ) - destroy_session(session, lib) - assert data_vector != data_matrix - - -def test_create_data_grid_dim(): - "Run the function to make sure it doesn't fail badly." - lib = load_libgmt() - session = create_session('test_create_data', lib) - # Grids from matrices using dim - create_data( - libgmt=lib, - session=session, - family='GMT_IS_GRID|GMT_VIA_MATRIX', - geometry='GMT_IS_SURFACE', - mode='GMT_CONTAINER_ONLY', - dim=[10, 20, 1, 0], - ) - destroy_session(session, lib) - - -def test_create_data_grid_range(): - "Run the function to make sure it doesn't fail badly." - lib = load_libgmt() - session = create_session('test_create_data', lib) - # Grids from matrices using range and int - create_data( - libgmt=lib, - session=session, - family='GMT_IS_GRID|GMT_VIA_MATRIX', - geometry='GMT_IS_SURFACE', - mode='GMT_CONTAINER_ONLY', - dim=[0, 0, 1, 0], - ranges=[150., 250., -20., 20.], - inc=[0.1, 0.2], - ) - destroy_session(session, lib) - - -def test_create_data_fails(): - "Test for failures on bad input" - lib = load_libgmt() - session = create_session('test_create_data', lib) - # Passing in invalid mode - with pytest.raises(GMTCLibError): - create_data( - libgmt=lib, - session=session, - family='GMT_IS_DATASET', - geometry='GMT_IS_SURFACE', - mode='Not_a_valid_mode', - dim=[0, 0, 1, 0], - ranges=[150., 250., -20., 20.], - inc=[0.1, 0.2], - ) - # Passing in invalid geometry - with pytest.raises(GMTCLibError): - create_data( - libgmt=lib, - session=session, - family='GMT_IS_GRID', - geometry='Not_a_valid_geometry', - mode='GMT_CONTAINER_ONLY', - dim=[0, 0, 1, 0], - ranges=[150., 250., -20., 20.], - inc=[0.1, 0.2], - ) - destroy_session(session, lib)