diff --git a/Makefile b/Makefile index 4d7bb63773e..f39f10d7e8f 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,8 @@ TESTDIR=tmp-test-dir-with-unique-name PYTEST_ARGS=--doctest-modules -v --pyargs PYTEST_COV_ARGS=--cov-config=../.coveragerc --cov-report=term-missing -CHECK_FILES=gmt setup.py +FORMAT_FILES=gmt setup.py doc/conf.py +LINT_FILES=gmt setup.py help: @echo "Commands:" @@ -28,18 +29,19 @@ test: coverage: # Run a tmp folder to make sure the tests are run on the installed version mkdir -p $(TESTDIR) - cd $(TESTDIR); python -c "import gmt; gmt.print_libgmt_info()" + @echo "" + @cd $(TESTDIR); python -c "import gmt; gmt.print_clib_info()" @echo "" cd $(TESTDIR); pytest $(PYTEST_COV_ARGS) --cov=gmt $(PYTEST_ARGS) gmt cp $(TESTDIR)/.coverage* . rm -r $(TESTDIR) format: - black $(CHECK_FILES) + black $(FORMAT_FILES) check: - black --check $(CHECK_FILES) - pylint $(CHECK_FILES) + black --check $(FORMAT_FILES) + pylint $(LINT_FILES) clean: find . -name "*.pyc" -exec rm -v {} \; diff --git a/doc/api/index.rst b/doc/api/index.rst index 71a73140563..a89c254b9f3 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -62,7 +62,7 @@ Miscellaneous which test - print_libgmt_info + print_clib_info Datasets @@ -100,37 +100,50 @@ All custom exceptions are derived from :class:`gmt.exceptions.GMTError`. GMT C API --------- -The :mod:`gmt.clib` package is a wrapper for the GMT C API built using -`ctypes `__. -Most calls to the C API happen through the :class:`gmt.clib.LibGMT` class. +The :mod:`gmt.clib` package is a wrapper for the GMT C API built using :mod:`ctypes`. +Most calls to the C API happen through the :class:`gmt.clib.Session` class. .. autosummary:: :toctree: generated - clib.LibGMT + clib.Session -Main methods (this is what the rest of the library uses): +`GMT modules `__ are executed through +the :meth:`~gmt.clib.Session.call_module` method: .. autosummary:: :toctree: generated - clib.LibGMT.call_module - clib.LibGMT.grid_to_vfile - clib.LibGMT.vectors_to_vfile - clib.LibGMT.matrix_to_vfile - clib.LibGMT.extract_region + clib.Session.call_module + +Passing memory blocks between Python variables (:class:`numpy.ndarray`, +:class:`pandas.Series`, and :class:`xarray.DataArray`) and GMT happens through *virtual +files*. These methods are context managers that automate the conversion of Python +variables to GMT virtual files: + +.. autosummary:: + :toctree: generated + + clib.Session.virtualfile_from_matrix + clib.Session.virtualfile_from_vectors + clib.Session.virtualfile_from_grid + Low level access (these are mostly used by the :mod:`gmt.clib` package): .. autosummary:: :toctree: generated - clib.LibGMT.create_session - clib.LibGMT.destroy_session - clib.LibGMT.get_constant - clib.LibGMT.get_default - clib.LibGMT.create_data - clib.LibGMT.open_virtual_file - clib.LibGMT.put_matrix - clib.LibGMT.put_vector - clib.LibGMT.write_data + clib.Session.create + clib.Session.destroy + clib.Session.__getitem__ + clib.Session.__enter__ + clib.Session.__exit__ + clib.Session.get_default + clib.Session.create_data + clib.Session.put_matrix + clib.Session.put_vector + clib.Session.write_data + clib.Session.open_virtual_file + clib.Session.extract_region + clib.Session.get_libgmt_func diff --git a/doc/conf.py b/doc/conf.py index 7de393b78aa..f02a62abd7d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -11,16 +11,17 @@ from gmt import __version__, __commit__ extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.doctest', - 'sphinx.ext.viewcode', - 'sphinx.ext.extlinks', - 'numpydoc', - 'nbsphinx', - 'gmt.sphinxext.gmtplot', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.doctest", + "sphinx.ext.viewcode", + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + "numpydoc", + "nbsphinx", + "gmt.sphinxext.gmtplot", ] # Autosummary pages will be generated by sphinx-autogen instead of sphinx-build @@ -28,36 +29,46 @@ numpydoc_class_members_toctree = False +# intersphinx configuration +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "numpy": ("https://docs.scipy.org/doc/numpy/", None), + "pandas": ("http://pandas.pydata.org/pandas-docs/stable/", None), + "xarray": ("http://xarray.pydata.org/en/stable/", None), +} + # Sphinx project configuration -templates_path = ['_templates'] -exclude_patterns = ['_build', '**.ipynb_checkpoints'] -source_suffix = '.rst' +templates_path = ["_templates"] +exclude_patterns = ["_build", "**.ipynb_checkpoints"] +source_suffix = ".rst" # The encoding of source files. -source_encoding = 'utf-8-sig' -master_doc = 'index' +source_encoding = "utf-8-sig" +master_doc = "index" # General information about the project year = datetime.date.today().year -project = u'GMT/Python' -copyright = u'2017-2018, Leonardo Uieda and Paul Wessel' -if len(__version__.split('+')) > 1 or __version__ == 'unknown': - version = 'dev' +project = u"GMT/Python" +copyright = u"2017-2018, Leonardo Uieda and Paul Wessel" +if len(__version__.split("+")) > 1 or __version__ == "unknown": + version = "dev" else: version = __version__ # These enable substitutions using |variable| in the rst files rst_epilog = """ .. |year| replace:: {year} -""".format(year=year) +""".format( + year=year +) -html_last_updated_fmt = '%b %d, %Y' -html_title = 'GMT/Python' -html_short_title = 'GMT/Python' -html_logo = '_static/gmt-python-logo.png' -html_favicon = '_static/favicon.png' -html_static_path = ['_static'] -html_extra_path = ['.nojekyll', 'CNAME'] -pygments_style = 'default' +html_last_updated_fmt = "%b %d, %Y" +html_title = "GMT/Python" +html_short_title = "GMT/Python" +html_logo = "_static/gmt-python-logo.png" +html_favicon = "_static/favicon.png" +html_static_path = ["_static"] +html_extra_path = [".nojekyll", "CNAME"] +pygments_style = "default" add_function_parentheses = False html_show_sourcelink = False html_show_sphinx = True @@ -65,22 +76,36 @@ # Theme config html_theme = "sphinx_rtd_theme" -html_theme_options = { -} +html_theme_options = {} html_context = { - 'menu_links': [ - (' Try it online!', 'http://try.gmtpython.xyz'), - (' Source Code', 'https://github.com/GenericMappingTools/gmt-python'), - (' Contributing', 'https://github.com/GenericMappingTools/gmt-python/blob/master/CONTRIBUTING.md'), - (' Code of Conduct', 'https://github.com/GenericMappingTools/gmt-python/blob/master/CODE_OF_CONDUCT.md'), - (' License', 'https://github.com/GenericMappingTools/gmt-python/blob/master/LICENSE.txt'), - (' Contact', 'https://gitter.im/GenericMappingTools/gmt-python'), + "menu_links": [ + (' Try it online!', "http://try.gmtpython.xyz"), + ( + ' Source Code', + "https://github.com/GenericMappingTools/gmt-python", + ), + ( + ' Contributing', + "https://github.com/GenericMappingTools/gmt-python/blob/master/CONTRIBUTING.md", + ), + ( + ' Code of Conduct', + "https://github.com/GenericMappingTools/gmt-python/blob/master/CODE_OF_CONDUCT.md", + ), + ( + ' License', + "https://github.com/GenericMappingTools/gmt-python/blob/master/LICENSE.txt", + ), + ( + ' Contact', + "https://gitter.im/GenericMappingTools/gmt-python", + ), ], # Custom variables to enable "Improve this page"" and "Download notebook" # links - 'doc_path': 'doc', - 'github_repo': 'GenericMappingTools/gmt-python', - 'github_version': 'master', + "doc_path": "doc", + "github_repo": "GenericMappingTools/gmt-python", + "github_version": "master", } # Load the custom CSS files (needs sphinx >= 1.6 for this to work) diff --git a/doc/index.rst b/doc/index.rst index 5195764f7fe..2f32e0a748a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -66,7 +66,7 @@ Project Goals * Make GMT more accessible to new users. * Build a Pythonic API for GMT. -* Interface with the GMT C API directly using :py:mod:`ctypes` (no system calls). +* Interface with the GMT C API directly using :mod:`ctypes` (no system calls). * Support for rich display in the `Jupyter notebook `__. * Integration with the Scipy stack: :class:`numpy.ndarray` or :class:`pandas.DataFrame` for data tables and :class:`xarray.DataArray` for grids. diff --git a/gmt/__init__.py b/gmt/__init__.py index c5ed977e798..161c18f3a43 100644 --- a/gmt/__init__.py +++ b/gmt/__init__.py @@ -27,26 +27,19 @@ _atexit.register(_end) -def print_libgmt_info(): +def print_clib_info(): """ - Print information about the currently loaded GMT shared library. + Print information about the GMT shared library that we can find. - Includes the GMT version, default values for parameters, the path to the - ``libgmt`` shared library, and GMT directories. + Includes the GMT version, default values for parameters, the path to the ``libgmt`` + shared library, and GMT directories. """ - import shutil - from .clib import LibGMT - - columns = shutil.get_terminal_size().columns - title = "Currently loaded libgmt" - left = (columns - len(title) - 2) // 2 - right = left + (columns - (2 * left + len(title) + 2)) - header = " ".join(["=" * left, title, "=" * right]) - - with LibGMT() as lib: - lines = [header] - for key in sorted(lib.info): - lines.append("{}: {}".format(key, lib.info[key])) + from .clib import Session + + lines = ["Loaded libgmt:"] + with Session() as ses: + for key in sorted(ses.info): + lines.append(" {}: {}".format(key, ses.info[key])) print("\n".join(lines)) @@ -83,7 +76,7 @@ def test(doctest=True, verbose=True, coverage=False, figures=True): """ import pytest - print_libgmt_info() + print_clib_info() args = [] if verbose: diff --git a/gmt/base_plotting.py b/gmt/base_plotting.py index e324785e90d..2badf4de48d 100644 --- a/gmt/base_plotting.py +++ b/gmt/base_plotting.py @@ -2,7 +2,7 @@ Base class with plot generating commands. Does not define any special non-GMT methods (savefig, show, etc). """ -from .clib import LibGMT +from .clib import Session from .exceptions import GMTInvalidInput from .helpers import ( build_arg_string, @@ -122,7 +122,7 @@ def coast(self, **kwargs): """ kwargs = self._preprocess(**kwargs) - with LibGMT() as lib: + with Session() as lib: lib.call_module("coast", build_arg_string(kwargs)) @fmt_docstring @@ -146,11 +146,11 @@ def grdimage(self, grid, **kwargs): """ kwargs = self._preprocess(**kwargs) kind = data_kind(grid, None, None) - with LibGMT() as lib: + with Session() as lib: if kind == "file": file_context = dummy_context(grid) elif kind == "grid": - file_context = lib.grid_to_vfile(grid) + file_context = lib.virtualfile_from_grid(grid) else: raise GMTInvalidInput("Unrecognized data type: {}".format(type(grid))) with file_context as fname: @@ -257,14 +257,14 @@ def plot(self, x=None, y=None, data=None, sizes=None, direction=None, **kwargs): ) extra_arrays.append(sizes) - with LibGMT() as lib: + with Session() as lib: # Choose how data will be passed in to the module if kind == "file": file_context = dummy_context(data) elif kind == "matrix": - file_context = lib.matrix_to_vfile(data) + file_context = lib.virtualfile_from_matrix(data) elif kind == "vectors": - file_context = lib.vectors_to_vfile(x, y, *extra_arrays) + file_context = lib.virtualfile_from_vectors(x, y, *extra_arrays) with file_context as fname: arg_str = " ".join([fname, build_arg_string(kwargs)]) @@ -316,7 +316,7 @@ def basemap(self, **kwargs): raise GMTInvalidInput("At least one of B, L, or T must be specified.") if "D" in kwargs and "F" not in kwargs: raise GMTInvalidInput("Option D requires F to be specified as well.") - with LibGMT() as lib: + with Session() as lib: lib.call_module("basemap", build_arg_string(kwargs)) @fmt_docstring @@ -351,5 +351,5 @@ def logo(self, **kwargs): kwargs = self._preprocess(**kwargs) if "D" not in kwargs: raise GMTInvalidInput("Option D must be specified.") - with LibGMT() as lib: + with Session() as lib: lib.call_module("logo", build_arg_string(kwargs)) diff --git a/gmt/clib/__init__.py b/gmt/clib/__init__.py index 59d041a2ca8..7d508095804 100644 --- a/gmt/clib/__init__.py +++ b/gmt/clib/__init__.py @@ -1,9 +1,9 @@ """ Low-level wrapper for the GMT C API. -The :class:`gmt.clib.LibGMT` class wraps the GMT C shared library (``libgmt``) +The :class:`gmt.clib.Session` class wraps the GMT C shared library (``libgmt``) with a pythonic interface. Access to the C library is done through :py:mod:`ctypes`. """ -from .core import LibGMT +from .session import Session diff --git a/gmt/clib/utils.py b/gmt/clib/conversion.py similarity index 66% rename from gmt/clib/utils.py rename to gmt/clib/conversion.py index 720d73c9569..08a77020513 100644 --- a/gmt/clib/utils.py +++ b/gmt/clib/conversion.py @@ -1,14 +1,10 @@ """ -Miscellaneous utilities +Functions to convert data types into ctypes friendly formats. """ -import os -import sys -import ctypes - import numpy as np import pandas -from ..exceptions import GMTOSError, GMTCLibError, GMTCLibNotFoundError, GMTInvalidInput +from ..exceptions import GMTInvalidInput def dataarray_to_matrix(grid): @@ -242,142 +238,6 @@ def _as_array(vector): return np.asarray(vector) -def load_libgmt(env=None): - """ - Find and load ``libgmt`` as a :py:class:`ctypes.CDLL`. - - By default, will look for the shared library in the directory specified by - the environment variable ``GMT_LIBRARY_PATH``. If it's not set, will let - ctypes try to find the library. - - Parameters - ---------- - env : dict or None - A dictionary containing the environment variables. If ``None``, will - default to ``os.environ``. - - Returns - ------- - :py:class:`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). - - """ - libpath = get_clib_path(env) - try: - libgmt = ctypes.CDLL(libpath) - check_libgmt(libgmt) - except OSError as err: - msg = "\n".join( - [ - "Couldn't find the GMT shared library '{}'.".format(libpath), - "Original error message:", - "{}".format(str(err)), - ] - ) - raise GMTCLibNotFoundError(msg) - return libgmt - - -def get_clib_path(env): - """ - Get the path to the libgmt shared library. - - Determine the file name and extension and append to the path set by - ``GMT_LIBRARY_PATH``, if any. - - Parameters - ---------- - env : dict or None - A dictionary containing the environment variables. If ``None``, will - default to ``os.environ``. - - Returns - ------- - libpath : str - The path to the libgmt shared library. - - """ - libname = ".".join(["libgmt", clib_extension()]) - if env is None: - env = os.environ - if "GMT_LIBRARY_PATH" in env: - libpath = os.path.join(env["GMT_LIBRARY_PATH"], libname) - else: - libpath = libname - return libpath - - -def clib_extension(os_name=None): - """ - Return the extension for the shared library for the current OS. - - .. warning:: - - Currently only works for macOS and Linux. - - Returns - ------- - os_name : str or None - The operating system name as given by ``sys.platform`` - (the default if None). - - Returns - ------- - ext : str - The extension ('.so', '.dylib', etc). - - """ - if os_name is None: - os_name = sys.platform - # Set the shared library extension in a platform independent way - if os_name.startswith("linux"): - lib_ext = "so" - elif os_name == "darwin": - # Darwin is macOS - lib_ext = "dylib" - else: - 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 : :py:class:`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 kwargs_to_ctypes_array(argument, kwargs, dtype): """ Convert an iterable argument from kwargs into a ctypes array variable. @@ -403,7 +263,7 @@ def kwargs_to_ctypes_array(argument, kwargs, dtype): >>> import ctypes as ct >>> value = kwargs_to_ctypes_array('bla', {'bla': [10, 10]}, ct.c_int*2) >>> type(value) - + >>> should_be_none = kwargs_to_ctypes_array( ... 'swallow', {'bla': 1, 'foo': [20, 30]}, ct.c_int*2) >>> print(should_be_none) diff --git a/gmt/clib/loading.py b/gmt/clib/loading.py new file mode 100644 index 00000000000..f1e07d8cac9 --- /dev/null +++ b/gmt/clib/loading.py @@ -0,0 +1,146 @@ +""" +Utility functions to load libgmt as ctypes.CDLL. + +The path to the shared library can be found automatically by ctypes or set through the +GMT_LIBRARY_PATH environment variable. +""" +import os +import sys +import ctypes + +from ..exceptions import GMTOSError, GMTCLibError, GMTCLibNotFoundError + + +def load_libgmt(env=None): + """ + Find and load ``libgmt`` as a :py:class:`ctypes.CDLL`. + + By default, will look for the shared library in the directory specified by + the environment variable ``GMT_LIBRARY_PATH``. If it's not set, will let + ctypes try to find the library. + + Parameters + ---------- + env : dict or None + A dictionary containing the environment variables. If ``None``, will + default to ``os.environ``. + + Returns + ------- + :py:class:`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). + + """ + libpath = get_clib_path(env) + try: + libgmt = ctypes.CDLL(libpath) + check_libgmt(libgmt) + except OSError as err: + msg = "\n".join( + [ + "Error loading the GMT shared library '{}':".format(libpath), + "{}".format(str(err)), + ] + ) + raise GMTCLibNotFoundError(msg) + return libgmt + + +def get_clib_path(env): + """ + Get the path to the libgmt shared library. + + Determine the file name and extension and append to the path set by + ``GMT_LIBRARY_PATH``, if any. + + Parameters + ---------- + env : dict or None + A dictionary containing the environment variables. If ``None``, will + default to ``os.environ``. + + Returns + ------- + libpath : str + The path to the libgmt shared library. + + """ + libname = ".".join(["libgmt", clib_extension()]) + if env is None: + env = os.environ + if "GMT_LIBRARY_PATH" in env: + libpath = os.path.join(env["GMT_LIBRARY_PATH"], libname) + else: + libpath = libname + return libpath + + +def clib_extension(os_name=None): + """ + Return the extension for the shared library for the current OS. + + .. warning:: + + Currently only works for macOS and Linux. + + Returns + ------- + os_name : str or None + The operating system name as given by ``sys.platform`` + (the default if None). + + Returns + ------- + ext : str + The extension ('.so', '.dylib', etc). + + """ + if os_name is None: + os_name = sys.platform + # Set the shared library extension in a platform independent way + if os_name.startswith("linux"): + lib_ext = "so" + elif os_name == "darwin": + # Darwin is macOS + lib_ext = "dylib" + else: + 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 : :py:class:`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) diff --git a/gmt/clib/core.py b/gmt/clib/session.py similarity index 61% rename from gmt/clib/core.py rename to gmt/clib/session.py index dd578e706aa..2d41432d8a8 100644 --- a/gmt/clib/core.py +++ b/gmt/clib/session.py @@ -1,8 +1,9 @@ """ -ctypes wrappers for core functions from the C API +Defines the Session class to create and destroy a GMT API session and provides access to +the API functions. Uses ctypes to wrap most of the core functions from the C API. """ import sys -import ctypes +import ctypes as ctp from contextlib import contextmanager from packaging.version import Version @@ -14,45 +15,74 @@ GMTInvalidInput, GMTVersionError, ) -from .utils import ( - load_libgmt, +from .loading import load_libgmt +from .conversion import ( kwargs_to_ctypes_array, vectors_to_arrays, dataarray_to_matrix, as_c_contiguous, ) +FAMILIES = [ + "GMT_IS_DATASET", + "GMT_IS_GRID", + "GMT_IS_PALETTE", + "GMT_IS_MATRIX", + "GMT_IS_VECTOR", +] -class LibGMT: # pylint: disable=too-many-instance-attributes +VIAS = ["GMT_VIA_MATRIX", "GMT_VIA_VECTOR"] + +GEOMETRIES = [ + "GMT_IS_NONE", + "GMT_IS_POINT", + "GMT_IS_LINE", + "GMT_IS_POLYGON", + "GMT_IS_PLP", + "GMT_IS_SURFACE", +] + +MODES = ["GMT_CONTAINER_ONLY", "GMT_OUTPUT"] + +REGISTRATIONS = ["GMT_GRID_PIXEL_REG", "GMT_GRID_NODE_REG"] + +DTYPES = { + "float64": "GMT_DOUBLE", + "float32": "GMT_FLOAT", + "int64": "GMT_LONG", + "int32": "GMT_INT", + "uint64": "GMT_ULONG", + "uint32": "GMT_UINT", +} + + +class Session: """ - Load and access the GMT shared library (libgmt). + A GMT API session where most operations involving the C API happen. - 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. + Works as a context manager (for use in a ``with`` block) to create a GMT C API + session and destroy it in the end to clean up memory. - Functions of the shared library are exposed as methods of this class. Most - methods MUST be used inside the context manager 'with' block. + Functions of the shared library are exposed as methods of this class. Most methods + MUST be used with an open session (inside a ``with`` block). If creating GMT data + structures to communicate data, put that code inside the same ``with`` block as the + API calls that will use the data. - If creating GMT data structures to communicate data, put that code inside - this context manager to reuse the same session. + By default, will let :mod:`ctypes` try to find the GMT shared library (``libgmt``). + If the environment variable ``GMT_LIBRARY_PATH`` is set, will look for the shared + library in the directory specified by it. - Requires a minimum version of GMT (see ``LibGMT.required_version``). Will - check for the version when entering the ``with`` block. A - ``GMTVersionError`` exception will be raised if the minimum version - requirements aren't met. + A ``GMTVersionError`` exception will be raised if the GMT shared library reports a + version < 6.0.0. - By default, will look for the shared library in the directory specified by - the environment variable ``GMT_LIBRARY_PATH``. If the variable is not set, - will let ctypes try to find the library. + The ``session_pointer`` attribute holds a ctypes pointer to the currently open + session. Raises ------ GMTCLibNotFoundError - If there was any problem loading the library (couldn't find it or - couldn't access the functions). + 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. GMTVersionError @@ -61,51 +91,33 @@ class LibGMT: # pylint: disable=too-many-instance-attributes Examples -------- - >>> with LibGMT() as lib: - ... lib.call_module('figure', 'my-figure') + >>> from gmt.datasets import load_earth_relief + >>> from gmt.helpers import GMTTempFile + >>> grid = load_earth_relief() + >>> type(grid) + + >>> # Create a session and destroy it automatically when exiting the "with" block. + >>> with Session() as ses: + ... # Create a virtual file and link to the memory block of the grid. + ... with ses.virtualfile_from_grid(grid) as fin: + ... # Create a temp file to use as output. + ... with GMTTempFile() as fout: + ... # Call the grdinfo module with the virtual file as input and the. + ... # temp file as output. + ... ses.call_module("grdinfo", "{} -C ->{}".format(fin, fout.name)) + ... # Read the contents of the temp file before it's deleted. + ... print(fout.read().strip()) + -180 180 -90 90 -8425 5551 1 1 361 181 """ - 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"] - - grid_registrations = ["GMT_GRID_PIXEL_REG", "GMT_GRID_NODE_REG"] - # The minimum version of GMT required required_version = "6.0.0" - # Map numpy dtypes to GMT types - _dtypes = { - "float64": "GMT_DOUBLE", - "float32": "GMT_FLOAT", - "int64": "GMT_LONG", - "int32": "GMT_INT", - "uint64": "GMT_ULONG", - "uint32": "GMT_UINT", - } - @property - def current_session(self): + def session_pointer(self): """ - The C void pointer for the current open GMT session. + The :class:`ctypes.c_void_p` pointer to the current open GMT session. Raises ------ @@ -114,49 +126,111 @@ def current_session(self): outside of the context manager). """ - if not hasattr(self, "_session_id") or self._session_id is None: - raise GMTCLibNoSessionError( - " ".join( - [ - "No currently open GMT API session.", - "Use only inside a 'with' block.", - ] - ) - ) - return self._session_id + if not hasattr(self, "_session_pointer") or self._session_pointer is None: + raise GMTCLibNoSessionError("No currently open GMT API session.") + return self._session_pointer - @current_session.setter - def current_session(self, session): + @session_pointer.setter + def session_pointer(self, session): """ Set the session void pointer. """ - self._session_id = session + self._session_pointer = session @property def info(self): + "Dictionary with the GMT version and default paths and parameters." + if not hasattr(self, "_info"): + self._info = { + "version": self.get_default("API_VERSION"), + "padding": self.get_default("API_PAD"), + "binary dir": self.get_default("API_BINDIR"), + "share dir": self.get_default("API_SHAREDIR"), + # This segfaults for some reason + # 'data dir': self.get_default("API_DATADIR"), + "plugin dir": self.get_default("API_PLUGINDIR"), + "library path": self.get_default("API_LIBRARY"), + "cores": self.get_default("API_CORES"), + "image layout": self.get_default("API_IMAGE_LAYOUT"), + "grid layout": self.get_default("API_GRID_LAYOUT"), + } + return self._info + + def __enter__(self): """ - Dictionary with the GMT version and default paths and parameters. + Create a GMT API session and check the libgmt version. + + Calls :meth:`~gmt.clib.Session.create`. + + Raises + ------ + GMTVersionError + If the version reported by libgmt is less than ``Session.required_version``. + Will destroy the session before raising the exception. + + """ + self.create("gmt-python-session") + # Need to store the version info because 'get_default' won't work after the + # session is destroyed. + version = self.info["version"] + if Version(version) < Version(self.required_version): + self.destroy() + raise GMTVersionError( + "Using an incompatible GMT version {}. Must be newer than {}.".format( + version, self.required_version + ) + ) + return self + + def __exit__(self, exc_type, exc_value, traceback): + """ + Destroy the currently open GMT API session. + + Calls :meth:`~gmt.clib.Session.destroy`. + """ + self.destroy() + + def __getitem__(self, name): """ - infodict = { - "version": self.get_default("API_VERSION"), - "padding": self.get_default("API_PAD"), - "binary dir": self.get_default("API_BINDIR"), - "share dir": self.get_default("API_SHAREDIR"), - # This segfaults for some reason - # 'data dir': self.get_default("API_DATADIR"), - "plugin dir": self.get_default("API_PLUGINDIR"), - "library path": self.get_default("API_LIBRARY"), - "cores": self.get_default("API_CORES"), - "image layout": self.get_default("API_IMAGE_LAYOUT"), - "grid layout": self.get_default("API_GRID_LAYOUT"), - } - return infodict + Get the value of a GMT 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. + + """ + c_get_enum = self.get_libgmt_func( + "GMT_Get_Enum", argtypes=[ctp.c_char_p], restype=ctp.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 get_libgmt_func(self, name, argtypes=None, restype=None): """ Get a ctypes function from the libgmt shared library. - Also assigns the argument and return type conversions to the function. + Assigns the argument and return type conversions for the function. + + Use this method to access a C function from libgmt. Parameters ---------- @@ -178,7 +252,7 @@ def get_libgmt_func(self, name, argtypes=None, restype=None): -------- >>> from ctypes import c_void_p, c_int - >>> with LibGMT() as lib: + >>> with Session() as lib: ... func = lib.get_libgmt_func('GMT_Destroy_Session', ... argtypes=[c_void_p], restype=c_int) >>> type(func) @@ -194,78 +268,64 @@ def get_libgmt_func(self, name, argtypes=None, restype=None): function.restype = restype return function - def __enter__(self): - """ - Start the GMT session and keep the session argument. + def create(self, name): """ - self.current_session = self.create_session("gmt-python-session") - # Need to store the version info because 'get_default' won't work after - # the session is destroyed. - version = self.info["version"] - if Version(version) < Version(self.required_version): - self._cleanup_session() - raise GMTVersionError( - "Using an incompatible GMT version {}. Must be newer than {}.".format( - version, self.required_version - ) - ) - return self + Create a new GMT C API session. - def __exit__(self, exc_type, exc_value, traceback): - """ - Destroy the session when exiting the context. - """ - self._cleanup_session() + This is required before most other methods of :class:`gmt.clib.Session` can be + called. - def _cleanup_session(self): - """ - Destroy the current session and set the stored session to None - """ - try: - self.destroy_session(self.current_session) - finally: - self.current_session = None + .. warning:: - def create_session(self, session_name): - """ - Create the ``GMTAPI_CTRL`` struct required by the GMT C API functions. + Usage of :class:`~gmt.clib.Session` as a context manager in a ``with`` block + is preferred over calling :meth:`~gmt.clib.Session.create` and + :meth:`~gmt.clib.Session.destroy` manually. - It is a C void pointer containing the current session information and - cannot be accessed directly. + Calls ``GMT_Create_Session`` and generates a new ``GMTAPI_CTRL`` struct, which + is a :class:`ctypes.c_void_p` pointer. Sets the ``session_pointer`` attribute to + this pointer. - Remember to terminate the current session using - :func:`gmt.clib.LibGMT.destroy_session` before creating a new one. + Remember to terminate the current session using :meth:`gmt.clib.Session.destroy` + before creating a new one. Parameters ---------- - session_name : str + 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. - """ + try: + # Won't raise an exception if there is a currently open session + self.session_pointer # pylint: disable=pointless-statement + # In this case, fail to create a new session until the old one is destroyed + raise GMTCLibError( + "Failed to create a GMT API session: There is a currently open session." + " Must destroy it fist." + ) + # If the exception is raised, this means that there is no open session and we're + # free to create a new one. + except GMTCLibNoSessionError: + pass + c_create_session = self.get_libgmt_func( "GMT_Create_Session", - argtypes=[ctypes.c_char_p, ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p], - restype=ctypes.c_void_p, + argtypes=[ctp.c_char_p, ctp.c_uint, ctp.c_uint, ctp.c_void_p], + restype=ctp.c_void_p, ) # Capture the output printed by GMT into this list. Will use it later to # generate error messages for the exceptions raised by API calls. - self._log = [] + self._error_log = [] - @ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_char_p) + @ctp.CFUNCTYPE(ctp.c_int, ctp.c_void_p, ctp.c_char_p) def print_func(file_pointer, message): # pylint: disable=unused-argument """ - Callback function that GMT uses to print log and error messages. - We'll capture the message and print it to stderr so that it will show up on - the Jupyter notebook. + Callback function that the GMT C API will use to print log and error + messages. We'll capture the messages and print them to stderr so that they + will show up on the Jupyter notebook. """ message = message.decode().strip() - self._log.append(message) + self._error_log.append(message) # flush to make sure the messages are printed even if we have a crash. print(message, file=sys.stderr, flush=True) return 0 @@ -274,86 +334,60 @@ def print_func(file_pointer, message): # pylint: disable=unused-argument # garbage collected otherwise self._print_callback = print_func - padding = self.get_constant("GMT_PAD_DEFAULT") - session_type = self.get_constant("GMT_SESSION_EXTERNAL") - session = c_create_session( - session_name.encode(), padding, session_type, print_func - ) + padding = self["GMT_PAD_DEFAULT"] + session_type = self["GMT_SESSION_EXTERNAL"] + session = c_create_session(name.encode(), padding, session_type, print_func) if session is None: - raise GMTCLibError("Failed to create a GMT API void pointer.") + raise GMTCLibError( + "Failed to create a GMT API session:\n{}".format(self._error_message) + ) - return session + self.session_pointer = session - def _get_error_message(self): + @property + def _error_message(self): """ - Return a string with error messages emitted by GMT. - Only includes messages with the string "[ERROR]" in them. + A string with all error messages emitted by the C API. + + Only includes messages with the string ``"[ERROR]"`` in them. """ msg = "" - if hasattr(self, "_log"): - msg = "\n".join(line for line in self._log if "[ERROR]" in line) + if hasattr(self, "_error_log"): + msg = "\n".join(line for line in self._error_log if "[ERROR]" in line) return msg - 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 : :py:class:`ctypes.CDLL` - The :py:class:`ctypes.CDLL` instance for the libgmt shared library. - + def destroy(self): """ - c_destroy_session = self.get_libgmt_func( - "GMT_Destroy_Session", argtypes=[ctypes.c_void_p], restype=ctypes.c_int - ) + Destroy the currently open GMT API session. - status = c_destroy_session(session) - if status: - raise GMTCLibError("Failed to destroy GMT API 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``. + .. warning:: - Parameters - ---------- - name : str - The name of the constant (e.g., ``"GMT_SESSION_EXTERNAL"``) + Usage of :class:`~gmt.clib.Session` as a context manager in a ``with`` block + is preferred over calling :meth:`~gmt.clib.Session.create` and + :meth:`~gmt.clib.Session.destroy` manually. - Returns - ------- - constant : int - Integer value of the constant. Do not rely on this value because it - might change. + Calls ``GMT_Destroy_Session`` to terminate and free the memory of a registered + ``GMTAPI_CTRL`` session (the pointer for this struct is stored in the + ``session_pointer`` attribute). - Raises - ------ - GMTCLibError - If the constant doesn't exist. + Always use this method after you are done using a C API session. The session + needs to be destroyed before creating a new one. Otherwise, some of the + configuration files might be left behind and can influence subsequent API calls. + Sets the ``session_pointer`` attribute to ``None``. """ - c_get_enum = self.get_libgmt_func( - "GMT_Get_Enum", argtypes=[ctypes.c_char_p], restype=ctypes.c_int + c_destroy_session = self.get_libgmt_func( + "GMT_Destroy_Session", argtypes=[ctp.c_void_p], restype=ctp.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)) + status = c_destroy_session(self.session_pointer) + if status: + raise GMTCLibError( + "Failed to destroy GMT API session:\n{}".format(self._error_message) + ) - return value + self.session_pointer = None def get_default(self, name): """ @@ -390,14 +424,14 @@ def get_default(self, name): """ c_get_default = self.get_libgmt_func( "GMT_Get_Default", - argtypes=[ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p], - restype=ctypes.c_int, + argtypes=[ctp.c_void_p, ctp.c_char_p, ctp.c_char_p], + restype=ctp.c_int, ) # Make a string buffer to get a return value - value = ctypes.create_string_buffer(10000) + value = ctp.create_string_buffer(10000) - status = c_get_default(self.current_session, name.encode(), value) + status = c_get_default(self.session_pointer, name.encode(), value) if status != 0: raise GMTCLibError( @@ -433,18 +467,18 @@ def call_module(self, module, args): """ c_call_module = self.get_libgmt_func( "GMT_Call_Module", - argtypes=[ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int, ctypes.c_void_p], - restype=ctypes.c_int, + argtypes=[ctp.c_void_p, ctp.c_char_p, ctp.c_int, ctp.c_void_p], + restype=ctp.c_int, ) - mode = self.get_constant("GMT_MODULE_CMD") + mode = self["GMT_MODULE_CMD"] status = c_call_module( - self.current_session, module.encode(), mode, args.encode() + self.session_pointer, module.encode(), mode, args.encode() ) if status != 0: raise GMTCLibError( "Module '{}' failed with status code {}:\n{}".format( - module, status, self._get_error_message() + module, status, self._error_message ) ) @@ -492,43 +526,40 @@ def create_data(self, family, geometry, mode, **kwargs): c_create_data = self.get_libgmt_func( "GMT_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, + ctp.c_void_p, # API + ctp.c_uint, # family + ctp.c_uint, # geometry + ctp.c_uint, # mode + ctp.POINTER(ctp.c_uint64), # dim + ctp.POINTER(ctp.c_double), # range + ctp.POINTER(ctp.c_double), # inc + ctp.c_uint, # registration + ctp.c_int, # pad + ctp.c_void_p, ], # data - restype=ctypes.c_void_p, + restype=ctp.c_void_p, ) - family_int = self._parse_constant( - family, valid=self.data_families, valid_modifiers=self.data_vias - ) + family_int = self._parse_constant(family, valid=FAMILIES, valid_modifiers=VIAS) mode_int = self._parse_constant( - mode, valid=self.data_modes, valid_modifiers=["GMT_GRID_IS_GEO"] + mode, valid=MODES, valid_modifiers=["GMT_GRID_IS_GEO"] ) - geometry_int = self._parse_constant(geometry, valid=self.data_geometries) + geometry_int = self._parse_constant(geometry, valid=GEOMETRIES) registration_int = self._parse_constant( - kwargs.get("registration", "GMT_GRID_NODE_REG"), - valid=self.grid_registrations, + kwargs.get("registration", "GMT_GRID_NODE_REG"), valid=REGISTRATIONS ) # Convert dim, ranges, and inc to ctypes arrays if given (will be None # if not given to represent NULL pointers) - 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) + dim = kwargs_to_ctypes_array("dim", kwargs, ctp.c_uint64 * 4) + ranges = kwargs_to_ctypes_array("ranges", kwargs, ctp.c_double * 4) + inc = kwargs_to_ctypes_array("inc", kwargs, ctp.c_double * 2) # Use a NULL pointer (None) for existing data to indicate that the # container should be created empty. Fill it in later using put_vector # and put_matrix. data_ptr = c_create_data( - self.current_session, + self.session_pointer, family_int, geometry_int, mode_int, @@ -558,7 +589,7 @@ def _parse_pad(self, family, kwargs): if "MATRIX" in family: pad = 0 else: - pad = self.get_constant("GMT_PAD_DEFAULT") + pad = self["GMT_PAD_DEFAULT"] return pad def _parse_constant(self, constant, valid, valid_modifiers=None): @@ -567,7 +598,7 @@ def _parse_constant(self, constant, valid, valid_modifiers=None): The GMT C API takes certain defined constants, like ``'GMT_IS_GRID'``, that need to be validated and converted to integer values using - :meth:`~gmt.clib.LibGMT.get_constant`. + :meth:`gmt.clib.Session.__getitem__`. The constants can also take a modifier by appending another constant name, e.g. ``'GMT_IS_GRID|GMT_VIA_MATRIX'``. The two parts must be @@ -616,7 +647,7 @@ def _parse_constant(self, constant, valid, valid_modifiers=None): parts[1], str(valid_modifiers) ) ) - integer_value = sum(self.get_constant(part) for part in parts) + integer_value = sum(self[part] for part in parts) return integer_value def _check_dtype_and_dim(self, array, ndim): @@ -647,18 +678,18 @@ def _check_dtype_and_dim(self, array, ndim): >>> import numpy as np >>> data = np.array([1, 2, 3], dtype='float64') - >>> with LibGMT() as lib: - ... gmttype = lib._check_dtype_and_dim(data, ndim=1) - ... gmttype == lib.get_constant('GMT_DOUBLE') + >>> with Session() as ses: + ... gmttype = ses._check_dtype_and_dim(data, ndim=1) + ... gmttype == ses["GMT_DOUBLE"] True >>> data = np.ones((5, 2), dtype='float32') - >>> with LibGMT() as lib: - ... gmttype = lib._check_dtype_and_dim(data, ndim=2) - ... gmttype == lib.get_constant('GMT_FLOAT') + >>> with Session() as ses: + ... gmttype = ses._check_dtype_and_dim(data, ndim=2) + ... gmttype == ses['GMT_FLOAT'] True """ - if array.dtype.name not in self._dtypes: + if array.dtype.name not in DTYPES: raise GMTInvalidInput( "Unsupported numpy data type '{}'.".format(array.dtype.name) ) @@ -666,7 +697,7 @@ def _check_dtype_and_dim(self, array, ndim): raise GMTInvalidInput( "Expected a numpy 1d array, got {}d.".format(array.ndim) ) - return self.get_constant(self._dtypes[array.dtype.name]) + return self[DTYPES[array.dtype.name]] def put_vector(self, dataset, column, vector): """ @@ -675,7 +706,7 @@ def put_vector(self, dataset, column, vector): Use this functions to attach numpy array data to a GMT dataset and pass it to GMT modules. Wraps ``GMT_Put_Vector``. - The dataset must be created by :meth:`~gmt.clib.LibGMT.create_data` + The dataset must be created by :meth:`~gmt.clib.Session.create_data` first. Use ``family='GMT_IS_DATASET|GMT_VIA_VECTOR'``. Not at all numpy dtypes are supported, only: float64, float32, int64, @@ -689,9 +720,9 @@ def put_vector(self, dataset, column, vector): Parameters ---------- - dataset : :py:class:`ctypes.c_void_p` + dataset : :class:`ctypes.c_void_p` The ctypes void pointer to a ``GMT_Dataset``. Create it with - :meth:`~gmt.clib.LibGMT.create_data`. + :meth:`~gmt.clib.Session.create_data`. column : int The column number of this vector in the dataset (starting from 0). vector : numpy 1d-array @@ -707,20 +738,14 @@ def put_vector(self, dataset, column, vector): """ c_put_vector = self.get_libgmt_func( "GMT_Put_Vector", - argtypes=[ - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_uint, - ctypes.c_uint, - ctypes.c_void_p, - ], - restype=ctypes.c_int, + argtypes=[ctp.c_void_p, ctp.c_void_p, ctp.c_uint, ctp.c_uint, ctp.c_void_p], + restype=ctp.c_int, ) gmt_type = self._check_dtype_and_dim(vector, ndim=1) - vector_pointer = vector.ctypes.data_as(ctypes.c_void_p) + vector_pointer = vector.ctypes.data_as(ctp.c_void_p) status = c_put_vector( - self.current_session, dataset, column, gmt_type, vector_pointer + self.session_pointer, dataset, column, gmt_type, vector_pointer ) if status != 0: raise GMTCLibError( @@ -739,7 +764,7 @@ def put_matrix(self, dataset, matrix, pad=0): Use this functions to attach numpy array data to a GMT dataset and pass it to GMT modules. Wraps ``GMT_Put_Matrix``. - The dataset must be created by :meth:`~gmt.clib.LibGMT.create_data` + The dataset must be created by :meth:`~gmt.clib.Session.create_data` first. Use ``|GMT_VIA_MATRIX'`` in the family. Not at all numpy dtypes are supported, only: float64, float32, int64, @@ -752,9 +777,9 @@ def put_matrix(self, dataset, matrix, pad=0): Parameters ---------- - dataset : :py:class:`ctypes.c_void_p` + dataset : :class:`ctypes.c_void_p` The ctypes void pointer to a ``GMT_Dataset``. Create it with - :meth:`~gmt.clib.LibGMT.create_data`. + :meth:`~gmt.clib.Session.create_data`. matrix : numpy 2d-array The array that will be attached to the dataset. Must be a 2d C contiguous array. @@ -771,20 +796,14 @@ def put_matrix(self, dataset, matrix, pad=0): """ c_put_matrix = self.get_libgmt_func( "GMT_Put_Matrix", - argtypes=[ - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_uint, - ctypes.c_int, - ctypes.c_void_p, - ], - restype=ctypes.c_int, + argtypes=[ctp.c_void_p, ctp.c_void_p, ctp.c_uint, ctp.c_int, ctp.c_void_p], + restype=ctp.c_int, ) gmt_type = self._check_dtype_and_dim(matrix, ndim=2) - matrix_pointer = matrix.ctypes.data_as(ctypes.c_void_p) + matrix_pointer = matrix.ctypes.data_as(ctp.c_void_p) status = c_put_matrix( - self.current_session, dataset, gmt_type, pad, matrix_pointer + self.session_pointer, dataset, gmt_type, pad, matrix_pointer ) if status != 0: raise GMTCLibError("Failed to put matrix of type {}.".format(matrix.dtype)) @@ -794,7 +813,7 @@ def write_data(self, family, geometry, mode, wesn, output, data): Write a GMT data container to a file. The data container should be created by - :meth:`~gmt.clib.LibGMT.create_data`. + :meth:`~gmt.clib.Session.create_data`. Wraps ``GMT_Write_Data`` but only allows writing to a file. So the ``method`` argument is omitted. @@ -818,9 +837,9 @@ def write_data(self, family, geometry, mode, wesn, output, data): elements. output : str The output file name. - data : :py:class:`ctypes.c_void_p` + data : :class:`ctypes.c_void_p` Pointer to the data container created by - :meth:`~gmt.clib.LibGMT.create_data`. + :meth:`~gmt.clib.Session.create_data`. Raises ------ @@ -832,29 +851,27 @@ def write_data(self, family, geometry, mode, wesn, output, data): c_write_data = self.get_libgmt_func( "GMT_Write_Data", argtypes=[ - ctypes.c_void_p, - ctypes.c_uint, - ctypes.c_uint, - ctypes.c_uint, - ctypes.c_uint, - ctypes.POINTER(ctypes.c_double), - ctypes.c_char_p, - ctypes.c_void_p, + ctp.c_void_p, + ctp.c_uint, + ctp.c_uint, + ctp.c_uint, + ctp.c_uint, + ctp.POINTER(ctp.c_double), + ctp.c_char_p, + ctp.c_void_p, ], - restype=ctypes.c_int, + restype=ctp.c_int, ) - family_int = self._parse_constant( - family, valid=self.data_families, valid_modifiers=self.data_vias - ) - geometry_int = self._parse_constant(geometry, valid=self.data_geometries) + family_int = self._parse_constant(family, valid=FAMILIES, valid_modifiers=VIAS) + geometry_int = self._parse_constant(geometry, valid=GEOMETRIES) status = c_write_data( - self.current_session, + self.session_pointer, family_int, - self.get_constant("GMT_IS_FILE"), + self["GMT_IS_FILE"], geometry_int, - self.get_constant(mode), - (ctypes.c_double * 6)(*wesn), + self[mode], + (ctp.c_double * 6)(*wesn), output.encode(), data, ) @@ -868,7 +885,7 @@ def open_virtual_file(self, family, geometry, direction, data): GMT uses a virtual file scheme to pass in data to API modules. Use it to pass in your GMT data structure (created using - :meth:`~gmt.clib.LibGMT.create_data`) to a module that expects an input + :meth:`~gmt.clib.Session.create_data`) to a module that expects an input or output file. Use in a ``with`` block. Will automatically close the virtual file when @@ -902,7 +919,7 @@ def open_virtual_file(self, family, geometry, direction, data): >>> import numpy as np >>> x = np.array([0, 1, 2, 3, 4]) >>> y = np.array([5, 6, 7, 8, 9]) - >>> with LibGMT() as lib: + >>> with Session() as lib: ... family = 'GMT_IS_DATASET|GMT_VIA_VECTOR' ... geometry = 'GMT_IS_POINT' ... dataset = lib.create_data( @@ -927,36 +944,34 @@ def open_virtual_file(self, family, geometry, direction, data): c_open_virtualfile = self.get_libgmt_func( "GMT_Open_VirtualFile", argtypes=[ - ctypes.c_void_p, - ctypes.c_uint, - ctypes.c_uint, - ctypes.c_uint, - ctypes.c_void_p, - ctypes.c_char_p, + ctp.c_void_p, + ctp.c_uint, + ctp.c_uint, + ctp.c_uint, + ctp.c_void_p, + ctp.c_char_p, ], - restype=ctypes.c_int, + restype=ctp.c_int, ) c_close_virtualfile = self.get_libgmt_func( "GMT_Close_VirtualFile", - argtypes=[ctypes.c_void_p, ctypes.c_char_p], - restype=ctypes.c_int, + argtypes=[ctp.c_void_p, ctp.c_char_p], + restype=ctp.c_int, ) - family_int = self._parse_constant( - family, valid=self.data_families, valid_modifiers=self.data_vias - ) - geometry_int = self._parse_constant(geometry, valid=self.data_geometries) + family_int = self._parse_constant(family, valid=FAMILIES, valid_modifiers=VIAS) + geometry_int = self._parse_constant(geometry, valid=GEOMETRIES) direction_int = self._parse_constant( direction, valid=["GMT_IN", "GMT_OUT"], valid_modifiers=["GMT_IS_REFERENCE", "GMT_IS_DUPLICATE"], ) - buff = ctypes.create_string_buffer(self.get_constant("GMT_STR16")) + buff = ctp.create_string_buffer(self["GMT_STR16"]) status = c_open_virtualfile( - self.current_session, family_int, geometry_int, direction_int, data, buff + self.session_pointer, family_int, geometry_int, direction_int, data, buff ) if status != 0: @@ -967,41 +982,39 @@ def open_virtual_file(self, family, geometry, direction, data): try: yield vfname finally: - status = c_close_virtualfile(self.current_session, vfname.encode()) + status = c_close_virtualfile(self.session_pointer, vfname.encode()) if status != 0: raise GMTCLibError("Failed to close virtual file '{}'.".format(vfname)) @contextmanager - def vectors_to_vfile(self, *vectors): + def virtualfile_from_vectors(self, *vectors): """ - Store 1d arrays in a GMT virtual file to use as a module input. + Store 1d arrays as columns of a table inside a virtual file. - Context manager (use in a ``with`` block). Yields the virtual file name - that you can pass as an argument to a GMT module call. Closes the - virtual file upon exit of the ``with`` block. + Use the virtual file name to pass in the data in your vectors to a GMT module. - Use this instead of creating GMT Datasets and Virtual Files by hand - with :meth:`~gmt.clib.LibGMT.create_data`, - :meth:`~gmt.clib.LibGMT.put_vector`, and - :meth:`~gmt.clib.LibGMT.open_virtual_file` + Context manager (use in a ``with`` block). Yields the virtual file name that you + can pass as an argument to a GMT module call. Closes the virtual file upon exit + of the ``with`` block. - The virtual file will contain the arrays as ``GMT Vector`` structures. + Use this instead of creating the data container and virtual file by hand with + :meth:`~gmt.clib.Session.create_data`, :meth:`~gmt.clib.Session.put_vector`, and + :meth:`~gmt.clib.Session.open_virtual_file`. - If the arrays are C contiguous blocks of memory, they will be passed - without copying to GMT. If they are not (e.g., they are columns of a 2D - array), they will need to be copied to a contiguous block. + If the arrays are C contiguous blocks of memory, they will be passed without + copying to GMT. If they are not (e.g., they are columns of a 2D array), they + will need to be copied to a contiguous block. Parameters ---------- vectors : 1d arrays - The vectors that will be included in the array. All must be of the - same size. + The vectors that will be included in the array. All must be of the same + size. Yields ------ - vfile : str - The name of virtual file. Pass this as a file name argument to a - GMT module. + fname : str + The name of virtual file. Pass this as a file name argument to a GMT module. Examples -------- @@ -1012,23 +1025,21 @@ def vectors_to_vfile(self, *vectors): >>> x = [1, 2, 3] >>> y = np.array([4, 5, 6]) >>> z = pd.Series([7, 8, 9]) - >>> with LibGMT() as lib: - ... with lib.vectors_to_vfile(x, y, z) as vfile: + >>> with Session() as ses: + ... with ses.virtualfile_from_vectors(x, y, z) as fin: ... # Send the output to a file so that we can read it - ... with GMTTempFile() as ofile: - ... args = '{} ->{}'.format(vfile, ofile.name) - ... lib.call_module('info', args) - ... print(ofile.read().strip()) + ... with GMTTempFile() as fout: + ... ses.call_module('info', '{} ->{}'.format(fin, fout.name)) + ... print(fout.read().strip()) : N = 3 <1/3> <4/6> <7/9> """ - # Conversion to a C-contiguous array needs to be done here and not in - # put_matrix because we need to maintain a reference to the copy while - # it is being used by the C API. Otherwise, the array would be garbage - # collected and the memory freed. Creating it in this context manager - # guarantees that the copy will be around until the virtual file is - # closed. - # The conversion is implicit in vectors_to_arrays. + # Conversion to a C-contiguous array needs to be done here and not in put_matrix + # because we need to maintain a reference to the copy while it is being used by + # the C API. Otherwise, the array would be garbage collected and the memory + # freed. Creating it in this context manager guarantees that the copy will be + # around until the virtual file is closed. The conversion is implicit in + # vectors_to_arrays. arrays = vectors_to_arrays(vectors) columns = len(arrays) @@ -1046,47 +1057,43 @@ def vectors_to_vfile(self, *vectors): for col, array in enumerate(arrays): self.put_vector(dataset, column=col, vector=array) - vf_args = (family, geometry, "GMT_IN", dataset) - with self.open_virtual_file(*vf_args) as vfile: + with self.open_virtual_file(family, geometry, "GMT_IN", dataset) as vfile: yield vfile @contextmanager - def matrix_to_vfile(self, matrix): + def virtualfile_from_matrix(self, matrix): """ - Store a 2d array in a GMT virtual file to use as a module input. + Store a 2d array as a table inside a virtual file. - Context manager (use in a ``with`` block). Yields the virtual file name - that you can pass as an argument to a GMT module call. Closes the - virtual file upon exit of the ``with`` block. + Use the virtual file name to pass in the data in your matrix to a GMT module. - The virtual file will contain the array as a ``GMT_MATRIX``. + Context manager (use in a ``with`` block). Yields the virtual file name that you + can pass as an argument to a GMT module call. Closes the virtual file upon exit + of the ``with`` block. - **Not meant for creating GMT Grids**. The grid requires more metadata - than just the data matrix. This creates a Dataset (table). + The virtual file will contain the array as a ``GMT_MATRIX`` pretending to be a + ``GMT_DATASET``. - Use this instead of creating GMT Datasets and Virtual Files by hand - with :meth:`~gmt.clib.LibGMT.create_data`, - :meth:`~gmt.clib.LibGMT.put_matrix`, and - :meth:`~gmt.clib.LibGMT.open_virtual_file` + **Not meant for creating ``GMT_GRID``**. The grid requires more metadata than + just the data matrix. Use :meth:`~gmt.clib.Session.virtualfile_from_grid` + instead. - The matrix must be C contiguous in memory. If it is not (e.g., it is a - slice of a larger array), the array will be copied to make sure it is. + Use this instead of creating the data container and virtual file by hand with + :meth:`~gmt.clib.Session.create_data`, :meth:`~gmt.clib.Session.put_matrix`, and + :meth:`~gmt.clib.Session.open_virtual_file` - It might be more efficient than using - :meth:`~gmt.clib.LibGMT.vectors_to_vfile` if your data are columns of a - 2D array. In these cases, ``vectors_to_vfile`` will have to duplicate - the memory of your array in order for columns to be C contiguous. + The matrix must be C contiguous in memory. If it is not (e.g., it is a slice of + a larger array), the array will be copied to make sure it is. Parameters ---------- matrix : 2d array - The matrix that will be included in the Dataset. + The matrix that will be included in the GMT data container. Yields ------ - vfile : str - The name of virtual file. Pass this as a file name argument to a - GMT module. + fname : str + The name of virtual file. Pass this as a file name argument to a GMT module. Examples -------- @@ -1099,22 +1106,20 @@ def matrix_to_vfile(self, matrix): [ 3 4 5] [ 6 7 8] [ 9 10 11]] - >>> with LibGMT() as lib: - ... with lib.matrix_to_vfile(data) as vfile: + >>> with Session() as ses: + ... with ses.virtualfile_from_matrix(data) as fin: ... # Send the output to a file so that we can read it - ... with GMTTempFile() as ofile: - ... args = '{} ->{}'.format(vfile, ofile.name) - ... lib.call_module('info', args) - ... print(ofile.read().strip()) + ... with GMTTempFile() as fout: + ... ses.call_module('info', '{} ->{}'.format(fin, fout.name)) + ... print(fout.read().strip()) : N = 4 <0/9> <1/10> <2/11> """ - # Conversion to a C-contiguous array needs to be done here and not in - # put_matrix because we need to maintain a reference to the copy while - # it is being used by the C API. Otherwise, the array would be garbage - # collected and the memory freed. Creating it in this context manager - # guarantees that the copy will be around until the virtual file is - # closed. + # Conversion to a C-contiguous array needs to be done here and not in put_matrix + # because we need to maintain a reference to the copy while it is being used by + # the C API. Otherwise, the array would be garbage collected and the memory + # freed. Creating it in this context manager guarantees that the copy will be + # around until the virtual file is closed. matrix = as_c_contiguous(matrix) rows, columns = matrix.shape @@ -1127,43 +1132,39 @@ def matrix_to_vfile(self, matrix): self.put_matrix(dataset, matrix) - vf_args = (family, geometry, "GMT_IN", dataset) - with self.open_virtual_file(*vf_args) as vfile: + with self.open_virtual_file(family, geometry, "GMT_IN", dataset) as vfile: yield vfile @contextmanager - def grid_to_vfile(self, grid): + def virtualfile_from_grid(self, grid): """ - Store a grid in a GMT virtual file to use as a module input. + Store a grid in a virtual file. - Used to pass grid data into GMT modules. Grids must be - ``xarray.DataArray`` instances. + Use the virtual file name to pass in the data in your grid to a GMT module. + Grids must be :class:`xarray.DataArray` instances. - Context manager (use in a ``with`` block). Yields the virtual file name - that you can pass as an argument to a GMT module call. Closes the - virtual file upon exit of the ``with`` block. + Context manager (use in a ``with`` block). Yields the virtual file name that you + can pass as an argument to a GMT module call. Closes the virtual file upon exit + of the ``with`` block. - The virtual file will contain the grid as a ``GMT_MATRIX``. + The virtual file will contain the grid as a ``GMT_MATRIX`` with extra metadata. - Use this instead of creating ``GMT_GRID`` and virtual files by hand - with :meth:`~gmt.clib.LibGMT.create_data`, - :meth:`~gmt.clib.LibGMT.put_matrix`, and - :meth:`~gmt.clib.LibGMT.open_virtual_file` + Use this instead of creating a data container and virtual file by hand with + :meth:`~gmt.clib.Session.create_data`, :meth:`~gmt.clib.Session.put_matrix`, and + :meth:`~gmt.clib.Session.open_virtual_file` - The grid data matrix must be C contiguous in memory. If it is not - (e.g., it is a slice of a larger array), the array will be copied to - make sure it is. + The grid data matrix must be C contiguous in memory. If it is not (e.g., it is a + slice of a larger array), the array will be copied to make sure it is. Parameters ---------- - grid : xarray.DataArraw + grid : :class:`xarray.DataArray` The grid that will be included in the virtual file. Yields ------ - vfile : str - The name of virtual file. Pass this as a file name argument to a - GMT module. + fname : str + The name of virtual file. Pass this as a file name argument to a GMT module. Examples -------- @@ -1179,24 +1180,23 @@ def grid_to_vfile(self, grid): -90.0 90.0 >>> print(data.values.min(), data.values.max()) -8425.0 5551.0 - >>> with LibGMT() as lib: - ... with lib.grid_to_vfile(data) as vfile: + >>> with Session() as ses: + ... with ses.virtualfile_from_grid(data) as fin: ... # Send the output to a file so that we can read it - ... with GMTTempFile() as ofile: - ... args = '{} -L0 -Cn ->{}'.format(vfile, ofile.name) - ... lib.call_module('grdinfo', args) - ... print(ofile.read().strip()) + ... with GMTTempFile() as fout: + ... args = '{} -L0 -Cn ->{}'.format(fin, fout.name) + ... ses.call_module('grdinfo', args) + ... print(fout.read().strip()) -180 180 -90 90 -8425 5551 1 1 361 181 >>> # The output is: w e s n z0 z1 dx dy n_columns n_rows """ - # Conversion to a C-contiguous array needs to be done here and not in - # put_matrix because we need to maintain a reference to the copy while - # it is being used by the C API. Otherwise, the array would be garbage - # collected and the memory freed. Creating it in this context manager - # guarantees that the copy will be around until the virtual file is - # closed. - # The conversion is implicit in dataarray_to_matrix. + # Conversion to a C-contiguous array needs to be done here and not in put_matrix + # because we need to maintain a reference to the copy while it is being used by + # the C API. Otherwise, the array would be garbage collected and the memory + # freed. Creating it in this context manager guarantees that the copy will be + # around until the virtual file is closed. The conversion is implicit in + # dataarray_to_matrix. matrix, region, inc = dataarray_to_matrix(grid) family = "GMT_IS_GRID|GMT_VIA_MATRIX" geometry = "GMT_IS_SURFACE" @@ -1228,7 +1228,7 @@ def extract_region(self): >>> fig = gmt.Figure() >>> fig.coast(region=[0, 10, -20, -10], projection="M6i", frame=True, ... land='black') - >>> with LibGMT() as lib: + >>> with Session() as lib: ... wesn = lib.extract_region() >>> print(', '.join(['{:.2f}'.format(x) for x in wesn])) 0.00, 10.00, -20.00, -10.00 @@ -1239,7 +1239,7 @@ def extract_region(self): >>> fig = gmt.Figure() >>> fig.coast(region='US.HI', projection="M6i", frame=True, ... land='black') - >>> with LibGMT() as lib: + >>> with Session() as lib: ... wesn = lib.extract_region() >>> print(', '.join(['{:.2f}'.format(x) for x in wesn])) -164.71, -154.81, 18.91, 23.58 @@ -1251,7 +1251,7 @@ def extract_region(self): >>> fig = gmt.Figure() >>> fig.coast(region='US.HI+r5', projection="M6i", frame=True, ... land='black') - >>> with LibGMT() as lib: + >>> with Session() as lib: ... wesn = lib.extract_region() >>> print(', '.join(['{:.2f}'.format(x) for x in wesn])) -165.00, -150.00, 15.00, 25.00 @@ -1259,20 +1259,16 @@ def extract_region(self): """ c_extract_region = self.get_libgmt_func( "GMT_Extract_Region", - argtypes=[ - ctypes.c_void_p, - ctypes.c_char_p, - ctypes.POINTER(ctypes.c_double), - ], - restype=ctypes.c_int, + argtypes=[ctp.c_void_p, ctp.c_char_p, ctp.POINTER(ctp.c_double)], + restype=ctp.c_int, ) wesn = np.empty(4, dtype=np.float64) - wesn_pointer = wesn.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) + wesn_pointer = wesn.ctypes.data_as(ctp.POINTER(ctp.c_double)) # The second argument to GMT_Extract_Region is a file pointer to a # PostScript file. It's only valid in classic mode. Use None to get a # NULL pointer instead. - status = c_extract_region(self.current_session, None, wesn_pointer) + status = c_extract_region(self.session_pointer, None, wesn_pointer) if status != 0: raise GMTCLibError("Failed to extract region from current figure.") return wesn diff --git a/gmt/figure.py b/gmt/figure.py index 6255c3e7b7c..f518157af56 100644 --- a/gmt/figure.py +++ b/gmt/figure.py @@ -10,7 +10,7 @@ except ImportError: Image = None -from .clib import LibGMT +from .clib import Session from .base_plotting import BasePlotting from .exceptions import GMTError, GMTInvalidInput from .helpers import ( @@ -81,7 +81,7 @@ def _activate_figure(self): """ # Passing format '-' tells gmt.end to not produce any files. fmt = "-" - with LibGMT() as lib: + with Session() as lib: lib.call_module("figure", "{} {}".format(self._name, fmt)) def _preprocess(self, **kwargs): @@ -96,7 +96,7 @@ def _preprocess(self, **kwargs): def region(self): "The geographic WESN bounding box for the current figure." self._activate_figure() - with LibGMT() as lib: + with Session() as lib: wesn = lib.extract_region() return wesn @@ -157,7 +157,7 @@ def psconvert(self, **kwargs): # Default cropping the figure to True if "A" not in kwargs: kwargs["A"] = "" - with LibGMT() as lib: + with Session() as lib: lib.call_module("psconvert", build_arg_string(kwargs)) def savefig( diff --git a/gmt/modules.py b/gmt/modules.py index c65ce460948..6d5148c40af 100644 --- a/gmt/modules.py +++ b/gmt/modules.py @@ -1,7 +1,7 @@ """ Non-plot GMT modules. """ -from .clib import LibGMT +from .clib import Session from .helpers import ( build_arg_string, fmt_docstring, @@ -35,11 +35,11 @@ def grdinfo(grid, **kwargs): """ kind = data_kind(grid, None, None) with GMTTempFile() as outfile: - with LibGMT() as lib: + with Session() as lib: if kind == "file": file_context = dummy_context(grid) elif kind == "grid": - file_context = lib.grid_to_vfile(grid) + file_context = lib.virtualfile_from_grid(grid) else: raise GMTInvalidInput("Unrecognized data type: {}".format(type(grid))) with file_context as infile: @@ -88,7 +88,7 @@ def info(fname, **kwargs): with GMTTempFile() as tmpfile: arg_str = " ".join([fname, build_arg_string(kwargs), "->" + tmpfile.name]) - with LibGMT() as lib: + with Session() as lib: lib.call_module("info", arg_str) return tmpfile.read() @@ -136,7 +136,7 @@ def which(fname, **kwargs): """ with GMTTempFile() as tmpfile: arg_str = " ".join([fname, build_arg_string(kwargs), "->" + tmpfile.name]) - with LibGMT() as lib: + with Session() as lib: lib.call_module("which", arg_str) path = tmpfile.read().strip() if not path: diff --git a/gmt/session_management.py b/gmt/session_management.py index 02e21cef402..c380310452f 100644 --- a/gmt/session_management.py +++ b/gmt/session_management.py @@ -1,7 +1,7 @@ """ Modern mode session management modules. """ -from .clib import LibGMT +from .clib import Session def begin(): @@ -14,7 +14,7 @@ def begin(): """ prefix = "gmt-python-session" - with LibGMT() as lib: + with Session() as lib: lib.call_module("begin", prefix) @@ -28,5 +28,5 @@ def end(): ``gmt.begin``), and bring the figures to the working directory. """ - with LibGMT() as lib: + with Session() as lib: lib.call_module("end", "") diff --git a/gmt/tests/test_clib.py b/gmt/tests/test_clib.py index b1094e0fd52..7f701696fe5 100644 --- a/gmt/tests/test_clib.py +++ b/gmt/tests/test_clib.py @@ -12,14 +12,10 @@ import xarray as xr from packaging.version import Version -from ..clib.core import LibGMT -from ..clib.utils import ( - clib_extension, - load_libgmt, - check_libgmt, - dataarray_to_matrix, - get_clib_path, -) +from .. import clib +from ..clib.session import FAMILIES, VIAS +from ..clib.loading import clib_extension, load_libgmt, check_libgmt, get_clib_path +from ..clib.conversion import dataarray_to_matrix from ..exceptions import ( GMTCLibError, GMTOSError, @@ -36,7 +32,7 @@ @contextmanager -def mock(lib, func, returns=None, mock_func=None): +def mock(session, func, returns=None, mock_func=None): """ Mock a GMT C API function to make it always return a given value. @@ -57,7 +53,7 @@ def mock_api_function(*args): # pylint: disable=unused-argument mock_func = mock_api_function - get_libgmt_func = lib.get_libgmt_func + get_libgmt_func = session.get_libgmt_func def mock_get_libgmt_func(name, argtypes=None, restype=None): """ @@ -67,10 +63,12 @@ def mock_get_libgmt_func(name, argtypes=None, restype=None): return mock_func return get_libgmt_func(name, argtypes, restype) - setattr(lib, "get_libgmt_func", mock_get_libgmt_func) + setattr(session, "get_libgmt_func", mock_get_libgmt_func) yield + setattr(session, "get_libgmt_func", get_libgmt_func) + def test_load_libgmt(): "Test that loading libgmt works and doesn't crash." @@ -87,7 +85,7 @@ def test_load_libgmt_fail(): def test_get_clib_path(): "Test that the correct path is found when setting GMT_LIBRARY_PATH." # Get the real path to the library first - with LibGMT() as lib: + with clib.Session() as lib: libpath = lib.info["library path"] libdir = os.path.dirname(libpath) # Assign it to the environment variable but keep a backup value to restore @@ -118,49 +116,70 @@ def test_clib_extension(): clib_extension("meh") -def test_constant(): +def test_getitem(): "Test that I can get correct constants from the C lib" - 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 - assert lib.get_constant("GMT_DOUBLE") != -99999 + ses = clib.Session() + assert ses["GMT_SESSION_EXTERNAL"] != -99999 + assert ses["GMT_MODULE_CMD"] != -99999 + assert ses["GMT_PAD_DEFAULT"] != -99999 + assert ses["GMT_DOUBLE"] != -99999 with pytest.raises(GMTCLibError): - lib.get_constant("A_WHOLE_LOT_OF_JUNK") + ses["A_WHOLE_LOT_OF_JUNK"] # pylint: disable=pointless-statement def test_create_destroy_session(): "Test that create and destroy session are called without errors" - lib = LibGMT() - session1 = lib.create_session(session_name="test_session1") - assert session1 is not None - session2 = lib.create_session(session_name="test_session2") - assert session2 is not None - assert session2 != session1 - lib.destroy_session(session1) - lib.destroy_session(session2) + # Create two session and make sure they are not pointing to the same memory + session1 = clib.Session() + session1.create(name="test_session1") + assert session1.session_pointer is not None + session2 = clib.Session() + session2.create(name="test_session2") + assert session2.session_pointer is not None + assert session2.session_pointer != session1.session_pointer + session1.destroy() + session2.destroy() + # Create and destroy a session twice + ses = clib.Session() + for __ in range(2): + with pytest.raises(GMTCLibNoSessionError): + ses.session_pointer # pylint: disable=pointless-statement + ses.create("session1") + assert ses.session_pointer is not None + ses.destroy() + with pytest.raises(GMTCLibNoSessionError): + ses.session_pointer # pylint: disable=pointless-statement def test_create_session_fails(): - "Check that an exception is raised if the session pointer is None" - lib = LibGMT() - with mock(lib, "GMT_Create_Session", returns=None): + "Check that an exception is raised when failing to create a session" + ses = clib.Session() + with mock(ses, "GMT_Create_Session", returns=None): with pytest.raises(GMTCLibError): - lib.create_session("test-session-name") + ses.create("test-session-name") + # Should also fail if trying to create a session before destroying the old one. + ses.create("test1") + with pytest.raises(GMTCLibError): + ses.create("test2") def test_destroy_session_fails(): "Fail to destroy session when given bad input" - lib = LibGMT() - with pytest.raises(GMTCLibError): - lib.destroy_session(None) + ses = clib.Session() + with pytest.raises(GMTCLibNoSessionError): + ses.destroy() + ses.create("test-session") + with mock(ses, "GMT_Destroy_Session", returns=1): + with pytest.raises(GMTCLibError): + ses.destroy() + ses.destroy() 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" - with LibGMT() as lib: + with clib.Session() as lib: with GMTTempFile() as out_fname: lib.call_module("info", "{} -C ->{}".format(data_fname, out_fname.name)) assert os.path.exists(out_fname.name) @@ -170,21 +189,21 @@ def test_call_module(): def test_call_module_invalid_arguments(): "Fails for invalid module arguments" - with LibGMT() as lib: + with clib.Session() as lib: with pytest.raises(GMTCLibError): lib.call_module("info", "bogus-data.bla") def test_call_module_invalid_name(): "Fails when given bad input" - with LibGMT() as lib: + with clib.Session() as lib: with pytest.raises(GMTCLibError): lib.call_module("meh", "") def test_call_module_error_message(): "Check is the GMT error message was captured." - with LibGMT() as lib: + with clib.Session() as lib: try: lib.call_module("info", "bogus-data.bla") except GMTCLibError as error: @@ -199,40 +218,36 @@ def test_call_module_error_message(): def test_method_no_session(): "Fails when not in a session" - # Create an instance of LibGMT without "with" so no session is created. - lib = LibGMT() + # Create an instance of clib.Session without "with" so no session is created. + lib = clib.Session() with pytest.raises(GMTCLibNoSessionError): lib.call_module("gmtdefaults", "") with pytest.raises(GMTCLibNoSessionError): - lib.current_session # pylint: disable=pointless-statement + lib.session_pointer # pylint: disable=pointless-statement def test_parse_constant_single(): "Parsing a single family argument correctly." - lib = LibGMT() - for family in lib.data_families: - parsed = lib._parse_constant(family, valid=lib.data_families) - assert parsed == lib.get_constant(family) + lib = clib.Session() + for family in FAMILIES: + parsed = lib._parse_constant(family, valid=FAMILIES) + assert parsed == lib[family] def test_parse_constant_composite(): "Parsing a composite constant argument (separated by |) correctly." - lib = LibGMT() - test_cases = ( - (family, via) for family in lib.data_families for via in lib.data_vias - ) + lib = clib.Session() + test_cases = ((family, via) for family in FAMILIES for via in VIAS) for family, via in test_cases: composite = "|".join([family, via]) - expected = lib.get_constant(family) + lib.get_constant(via) - parsed = lib._parse_constant( - composite, valid=lib.data_families, valid_modifiers=lib.data_vias - ) + expected = lib[family] + lib[via] + parsed = lib._parse_constant(composite, valid=FAMILIES, valid_modifiers=VIAS) assert parsed == expected def test_parse_constant_fails(): "Check if the function fails when given bad input" - lib = LibGMT() + lib = clib.Session() test_cases = [ "SOME_random_STRING", "GMT_IS_DATASET|GMT_VIA_MATRIX|GMT_VIA_VECTOR", @@ -242,29 +257,23 @@ def test_parse_constant_fails(): ] for test_case in test_cases: with pytest.raises(GMTInvalidInput): - lib._parse_constant( - test_case, valid=lib.data_families, valid_modifiers=lib.data_vias - ) + lib._parse_constant(test_case, valid=FAMILIES, valid_modifiers=VIAS) # Should also fail if not given valid modifiers but is using them anyway. # This should work... lib._parse_constant( - "GMT_IS_DATASET|GMT_VIA_MATRIX", - valid=lib.data_families, - valid_modifiers=lib.data_vias, + "GMT_IS_DATASET|GMT_VIA_MATRIX", valid=FAMILIES, valid_modifiers=VIAS ) # But this shouldn't. with pytest.raises(GMTInvalidInput): lib._parse_constant( - "GMT_IS_DATASET|GMT_VIA_MATRIX", - valid=lib.data_families, - valid_modifiers=None, + "GMT_IS_DATASET|GMT_VIA_MATRIX", valid=FAMILIES, valid_modifiers=None ) def test_create_data_dataset(): "Run the function to make sure it doesn't fail badly." - with LibGMT() as lib: + with clib.Session() as lib: # Dataset from vectors data_vector = lib.create_data( family="GMT_IS_DATASET|GMT_VIA_VECTOR", @@ -284,7 +293,7 @@ def test_create_data_dataset(): def test_create_data_grid_dim(): "Create a grid ignoring range and inc." - with LibGMT() as lib: + with clib.Session() as lib: # Grids from matrices using dim lib.create_data( family="GMT_IS_GRID|GMT_VIA_MATRIX", @@ -296,7 +305,7 @@ def test_create_data_grid_dim(): def test_create_data_grid_range(): "Create a grid specifying range and inc instead of dim." - with LibGMT() as lib: + with clib.Session() as lib: # Grids from matrices using range and int lib.create_data( family="GMT_IS_GRID|GMT_VIA_MATRIX", @@ -311,7 +320,7 @@ def test_create_data_fails(): "Check that create_data raises exceptions for invalid input and output" # Passing in invalid mode with pytest.raises(GMTInvalidInput): - with LibGMT() as lib: + with clib.Session() as lib: lib.create_data( family="GMT_IS_DATASET", geometry="GMT_IS_SURFACE", @@ -322,7 +331,7 @@ def test_create_data_fails(): ) # Passing in invalid geometry with pytest.raises(GMTInvalidInput): - with LibGMT() as lib: + with clib.Session() as lib: lib.create_data( family="GMT_IS_GRID", geometry="Not_a_valid_geometry", @@ -334,7 +343,7 @@ def test_create_data_fails(): # If the data pointer returned is None (NULL pointer) with pytest.raises(GMTCLibError): - with LibGMT() as lib: + with clib.Session() as lib: with mock(lib, "GMT_Create_Data", returns=None): lib.create_data( family="GMT_IS_DATASET", @@ -348,7 +357,7 @@ def test_put_vector(): "Check that assigning a numpy array to a dataset works" dtypes = "float32 float64 int32 int64 uint32 uint64".split() for dtype in dtypes: - with LibGMT() as lib: + with clib.Session() as lib: dataset = lib.create_data( family="GMT_IS_DATASET|GMT_VIA_VECTOR", geometry="GMT_IS_POINT", @@ -358,9 +367,9 @@ def test_put_vector(): x = np.array([1, 2, 3, 4, 5], dtype=dtype) y = np.array([6, 7, 8, 9, 10], dtype=dtype) z = np.array([11, 12, 13, 14, 15], dtype=dtype) - lib.put_vector(dataset, column=lib.get_constant("GMT_X"), vector=x) - lib.put_vector(dataset, column=lib.get_constant("GMT_Y"), vector=y) - lib.put_vector(dataset, column=lib.get_constant("GMT_Z"), vector=z) + lib.put_vector(dataset, column=lib["GMT_X"], vector=x) + lib.put_vector(dataset, column=lib["GMT_Y"], vector=y) + lib.put_vector(dataset, column=lib["GMT_Z"], vector=z) # Turns out wesn doesn't matter for Datasets wesn = [0] * 6 # Save the data to a file to see if it's being accessed correctly @@ -382,7 +391,7 @@ def test_put_vector(): def test_put_vector_invalid_dtype(): "Check that it fails with an exception for invalid data types" - with LibGMT() as lib: + with clib.Session() as lib: dataset = lib.create_data( family="GMT_IS_DATASET|GMT_VIA_VECTOR", geometry="GMT_IS_POINT", @@ -396,7 +405,7 @@ def test_put_vector_invalid_dtype(): def test_put_vector_wrong_column(): "Check that it fails with an exception when giving an invalid column" - with LibGMT() as lib: + with clib.Session() as lib: dataset = lib.create_data( family="GMT_IS_DATASET|GMT_VIA_VECTOR", geometry="GMT_IS_POINT", @@ -410,7 +419,7 @@ def test_put_vector_wrong_column(): def test_put_vector_2d_fails(): "Check that it fails with an exception for multidimensional arrays" - with LibGMT() as lib: + with clib.Session() as lib: dataset = lib.create_data( family="GMT_IS_DATASET|GMT_VIA_VECTOR", geometry="GMT_IS_POINT", @@ -427,7 +436,7 @@ def test_put_matrix(): dtypes = "float32 float64 int32 int64 uint32 uint64".split() shape = (3, 4) for dtype in dtypes: - with LibGMT() as lib: + with clib.Session() as lib: dataset = lib.create_data( family="GMT_IS_DATASET|GMT_VIA_MATRIX", geometry="GMT_IS_POINT", @@ -458,7 +467,7 @@ def test_put_matrix_fails(): # It's hard to make put_matrix fail on the C API level because of all the # checks on input arguments. Mock the C API function just to make sure it # works. - with LibGMT() as lib: + with clib.Session() as lib: with mock(lib, "GMT_Put_Matrix", returns=1): with pytest.raises(GMTCLibError): lib.put_matrix(dataset=None, matrix=np.empty((10, 2)), pad=0) @@ -471,7 +480,7 @@ def test_put_matrix_grid(): inc = [1, 1] shape = ((wesn[3] - wesn[2]) // inc[1] + 1, (wesn[1] - wesn[0]) // inc[0] + 1) for dtype in dtypes: - with LibGMT() as lib: + with clib.Session() as lib: grid = lib.create_data( family="GMT_IS_GRID|GMT_VIA_MATRIX", geometry="GMT_IS_SURFACE", @@ -502,7 +511,7 @@ def test_virtual_file(): dtypes = "float32 float64 int32 int64 uint32 uint64".split() shape = (5, 3) for dtype in dtypes: - with LibGMT() as lib: + with clib.Session() as lib: family = "GMT_IS_DATASET|GMT_VIA_MATRIX" geometry = "GMT_IS_POINT" dataset = lib.create_data( @@ -536,7 +545,7 @@ def test_virtual_file_fails(): # Mock Open_VirtualFile to test the status check when entering the context. # If the exception is raised, the code won't get to the closing of the # virtual file. - with LibGMT() as lib, mock(lib, "GMT_Open_VirtualFile", returns=1): + with clib.Session() as lib, mock(lib, "GMT_Open_VirtualFile", returns=1): with pytest.raises(GMTCLibError): with lib.open_virtual_file(*vfargs): print("Should not get to this code") @@ -544,7 +553,7 @@ def test_virtual_file_fails(): # Test the status check when closing the virtual file # Mock the opening to return 0 (success) so that we don't open a file that # we won't close later. - with LibGMT() as lib, mock(lib, "GMT_Open_VirtualFile", returns=0), mock( + with clib.Session() as lib, mock(lib, "GMT_Open_VirtualFile", returns=0), mock( lib, "GMT_Close_VirtualFile", returns=1 ): with pytest.raises(GMTCLibError): @@ -555,7 +564,7 @@ def test_virtual_file_fails(): def test_virtual_file_bad_direction(): "Test passing an invalid direction argument" - with LibGMT() as lib: + with clib.Session() as lib: vfargs = ( "GMT_IS_DATASET|GMT_VIA_MATRIX", "GMT_IS_POINT", @@ -567,7 +576,7 @@ def test_virtual_file_bad_direction(): print("This should have failed") -def test_vectors_to_vfile(): +def test_virtualfile_from_vectors(): "Test the automation for transforming vectors to virtual file dataset" dtypes = "float32 float64 int32 int64 uint32 uint64".split() size = 10 @@ -575,8 +584,8 @@ def test_vectors_to_vfile(): x = np.arange(size, dtype=dtype) y = np.arange(size, size * 2, 1, dtype=dtype) z = np.arange(size * 2, size * 3, 1, dtype=dtype) - with LibGMT() as lib: - with lib.vectors_to_vfile(x, y, z) as vfile: + with clib.Session() as lib: + with lib.virtualfile_from_vectors(x, y, z) as vfile: with GMTTempFile() as outfile: lib.call_module("info", "{} ->{}".format(vfile, outfile.name)) output = outfile.read(keep_tabs=True) @@ -587,14 +596,14 @@ def test_vectors_to_vfile(): assert output == expected -def test_vectors_to_vfile_transpose(): +def test_virtualfile_from_vectors_transpose(): "Test transforming matrix columns to virtual file dataset" dtypes = "float32 float64 int32 int64 uint32 uint64".split() shape = (7, 5) for dtype in dtypes: data = np.arange(shape[0] * shape[1], dtype=dtype).reshape(shape) - with LibGMT() as lib: - with lib.vectors_to_vfile(*data.T) as vfile: + with clib.Session() as lib: + with lib.virtualfile_from_vectors(*data.T) as vfile: with GMTTempFile() as outfile: lib.call_module("info", "{} -C ->{}".format(vfile, outfile.name)) output = outfile.read(keep_tabs=True) @@ -605,24 +614,24 @@ def test_vectors_to_vfile_transpose(): assert output == expected -def test_vectors_to_vfile_diff_size(): +def test_virtualfile_from_vectors_diff_size(): "Test the function fails for arrays of different sizes" x = np.arange(5) y = np.arange(6) - with LibGMT() as lib: + with clib.Session() as lib: with pytest.raises(GMTInvalidInput): - with lib.vectors_to_vfile(x, y): + with lib.virtualfile_from_vectors(x, y): print("This should have failed") -def test_matrix_to_vfile(): +def test_virtualfile_from_matrix(): "Test transforming a matrix to virtual file dataset" dtypes = "float32 float64 int32 int64 uint32 uint64".split() shape = (7, 5) for dtype in dtypes: data = np.arange(shape[0] * shape[1], dtype=dtype).reshape(shape) - with LibGMT() as lib: - with lib.matrix_to_vfile(data) as vfile: + with clib.Session() as lib: + with lib.virtualfile_from_matrix(data) as vfile: with GMTTempFile() as outfile: lib.call_module("info", "{} ->{}".format(vfile, outfile.name)) output = outfile.read(keep_tabs=True) @@ -633,7 +642,7 @@ def test_matrix_to_vfile(): assert output == expected -def test_matrix_to_vfile_slice(): +def test_virtualfile_from_matrix_slice(): "Test transforming a slice of a larger array to virtual file dataset" dtypes = "float32 float64 int32 int64 uint32 uint64".split() shape = (10, 6) @@ -642,8 +651,8 @@ def test_matrix_to_vfile_slice(): rows = 5 cols = 3 data = full_data[:rows, :cols] - with LibGMT() as lib: - with lib.matrix_to_vfile(data) as vfile: + with clib.Session() as lib: + with lib.virtualfile_from_matrix(data) as vfile: with GMTTempFile() as outfile: lib.call_module("info", "{} ->{}".format(vfile, outfile.name)) output = outfile.read(keep_tabs=True) @@ -654,7 +663,7 @@ def test_matrix_to_vfile_slice(): assert output == expected -def test_vectors_to_vfile_pandas(): +def test_virtualfile_from_vectors_pandas(): "Pass vectors to a dataset using pandas Series" dtypes = "float32 float64 int32 int64 uint32 uint64".split() size = 13 @@ -666,8 +675,8 @@ def test_vectors_to_vfile_pandas(): z=np.arange(size * 2, size * 3, 1, dtype=dtype), ) ) - with LibGMT() as lib: - with lib.vectors_to_vfile(data.x, data.y, data.z) as vfile: + with clib.Session() as lib: + with lib.virtualfile_from_vectors(data.x, data.y, data.z) as vfile: with GMTTempFile() as outfile: lib.call_module("info", "{} ->{}".format(vfile, outfile.name)) output = outfile.read(keep_tabs=True) @@ -681,14 +690,14 @@ def test_vectors_to_vfile_pandas(): assert output == expected -def test_vectors_to_vfile_arraylike(): +def test_virtualfile_from_vectors_arraylike(): "Pass array-like vectors to a dataset" size = 13 x = list(range(0, size, 1)) y = tuple(range(size, size * 2, 1)) z = range(size * 2, size * 3, 1) - with LibGMT() as lib: - with lib.vectors_to_vfile(x, y, z) as vfile: + with clib.Session() as lib: + with lib.virtualfile_from_vectors(x, y, z) as vfile: with GMTTempFile() as outfile: lib.call_module("info", "{} ->{}".format(vfile, outfile.name)) output = outfile.read(keep_tabs=True) @@ -703,7 +712,7 @@ def test_extract_region_fails(): "Check that extract region fails if nothing has been plotted." Figure() with pytest.raises(GMTCLibError): - with LibGMT() as lib: + with clib.Session() as lib: lib.extract_region() @@ -720,16 +729,16 @@ def test_extract_region_two_figures(): # Activate the first figure and extract the region from it # Use in a different session to avoid any memory problems. - with LibGMT() as lib: + with clib.Session() as lib: lib.call_module("figure", "{} -".format(fig1._name)) - with LibGMT() as lib: + with clib.Session() as lib: wesn1 = lib.extract_region() npt.assert_allclose(wesn1, region1) # Now try it with the second one - with LibGMT() as lib: + with clib.Session() as lib: lib.call_module("figure", "{} -".format(fig2._name)) - with LibGMT() as lib: + with clib.Session() as lib: wesn2 = lib.extract_region() npt.assert_allclose(wesn2, np.array([-165., -150., 15., 25.])) @@ -740,7 +749,7 @@ def test_write_data_fails(): # Fault. Can't test this if by giving a bad file name because if # output=='', GMT will just write to stdout and spaces are valid file # names. Use a mock instead just to exercise this part of the code. - with LibGMT() as lib: + with clib.Session() as lib: with mock(lib, "GMT_Write_Data", returns=1): with pytest.raises(GMTCLibError): lib.write_data( @@ -777,7 +786,7 @@ def test_dataarray_to_matrix_inc_fails(): def test_get_default(): "Make sure get_default works without crashing and gives reasonable results" - with LibGMT() as lib: + with clib.Session() as lib: assert lib.get_default("API_GRID_LAYOUT") in ["rows", "columns"] assert int(lib.get_default("API_CORES")) >= 1 assert Version(lib.get_default("API_VERSION")) >= Version("6.0.0") @@ -785,16 +794,16 @@ def test_get_default(): def test_get_default_fails(): "Make sure get_default raises an exception for invalid names" - with LibGMT() as lib: + with clib.Session() as lib: with pytest.raises(GMTCLibError): lib.get_default("NOT_A_VALID_NAME") def test_info_dict(): - "Make sure the LibGMT.info dict is working." + "Make sure the clib.Session.info dict is working." # Check if there are no errors or segfaults from getting all of the # properties. - with LibGMT() as lib: + with clib.Session() as lib: assert lib.info # Mock GMT_Get_Default to return always the same string @@ -803,17 +812,18 @@ def mock_defaults(api, name, value): # pylint: disable=unused-argument value.value = b"bla" return 0 - with LibGMT() as lib: - with mock(lib, "GMT_Get_Default", mock_func=mock_defaults): - info = lib.info - # Check for an empty dictionary - assert info - for key in info: - assert info[key] == "bla" + ses = clib.Session() + ses.create("test-session") + with mock(ses, "GMT_Get_Default", mock_func=mock_defaults): + # Check for an empty dictionary + assert ses.info + for key in ses.info: + assert ses.info[key] == "bla" + ses.destroy() def test_fails_for_wrong_version(): - "Make sure the LibGMT raises an exception if GMT is too old" + "Make sure the clib.Session raises an exception if GMT is too old" # Mock GMT_Get_Default to return an old version def mock_defaults(api, name, value): # pylint: disable=unused-argument @@ -824,11 +834,11 @@ def mock_defaults(api, name, value): # pylint: disable=unused-argument value.value = b"bla" return 0 - lib = LibGMT() + lib = clib.Session() with mock(lib, "GMT_Get_Default", mock_func=mock_defaults): with pytest.raises(GMTVersionError): with lib: assert lib.info["version"] != "5.4.3" # Make sure the session is closed when the exception is raised. with pytest.raises(GMTCLibNoSessionError): - assert lib.current_session + assert lib.session_pointer diff --git a/gmt/tests/test_session_management.py b/gmt/tests/test_session_management.py index b421be8a7c5..34fa8ffa5bd 100644 --- a/gmt/tests/test_session_management.py +++ b/gmt/tests/test_session_management.py @@ -4,7 +4,7 @@ import os from ..session_management import begin, end -from ..clib import LibGMT +from ..clib import Session def test_begin_end(): @@ -14,7 +14,7 @@ def test_begin_end(): """ end() # Kill the global session begin() - with LibGMT() as lib: + with Session() as lib: lib.call_module("psbasemap", "-R10/70/-3/8 -JX4i/3i -Ba") end() begin() # Restart the global session