Skip to content

Commit

Permalink
Improve how PyGMT finds the GMT library (#440)
Browse files Browse the repository at this point in the history
This PR improves how PyGMT finds the GMT library on Windows, using the `ctypes.util.find_library` function, which searches DLLs in the system search path, (i.e., the variable PATH).

Changes in this PR:

- Remove the variable **GMT_LIBRARY_PATH** from the CI settings
- Use `find_library` to search DLLs on Windows
- Fix function `clib_name` to `clib_names`
- Improve the tests
  • Loading branch information
seisman authored May 21, 2020
1 parent 6d4c695 commit ce5de0e
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 30 deletions.
2 changes: 0 additions & 2 deletions .azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,6 @@ jobs:
CONDA_REQUIREMENTS: requirements.txt
CONDA_REQUIREMENTS_DEV: requirements-dev.txt
CONDA_INSTALL_EXTRA: "codecov gmt=6.0.0"
# ctypes.CDLL cannot find conda's libraries
GMT_LIBRARY_PATH: 'C:\Miniconda\envs\testing\Library\bin'

strategy:
matrix:
Expand Down
65 changes: 45 additions & 20 deletions pygmt/clib/loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,19 @@
import os
import sys
import ctypes
from ctypes.util import find_library

from ..exceptions import GMTOSError, GMTCLibError, GMTCLibNotFoundError


def load_libgmt(env=None):
def load_libgmt():
"""
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
Expand All @@ -37,27 +32,26 @@ def load_libgmt(env=None):
couldn't access the functions).
"""
if env is None:
env = os.environ
libnames = clib_name(os_name=sys.platform)
libpath = env.get("GMT_LIBRARY_PATH", "")
lib_fullnames = clib_full_names()
error = True
for libname in libnames:
for libname in lib_fullnames:
try:
libgmt = ctypes.CDLL(os.path.join(libpath, libname))
libgmt = ctypes.CDLL(libname)
check_libgmt(libgmt)
error = False
break
except OSError as err:
error = err
if error:
raise GMTCLibNotFoundError(
"Error loading the GMT shared library '{}':".format(", ".join(libnames))
"Error loading the GMT shared library '{}':".format(
", ".join(lib_fullnames)
)
)
return libgmt


def clib_name(os_name):
def clib_names(os_name):
"""
Return the name of GMT's shared library for the current OS.
Expand All @@ -68,20 +62,51 @@ def clib_name(os_name):
Returns
-------
libname : list of str
libnames : list of str
List of possible names of GMT's shared library.
"""
if os_name.startswith("linux"):
libname = ["libgmt.so"]
libnames = ["libgmt.so"]
elif os_name == "darwin":
# Darwin is macOS
libname = ["libgmt.dylib"]
libnames = ["libgmt.dylib"]
elif os_name == "win32":
libname = ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
libnames = ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
else:
raise GMTOSError('Operating system "{}" not supported.'.format(sys.platform))
return libname
return libnames


def clib_full_names(env=None):
"""
Return the full path of GMT's shared library for the current OS.
Parameters
----------
env : dict or None
A dictionary containing the environment variables. If ``None``, will
default to ``os.environ``.
Returns
-------
lib_fullnames: list of str
List of possible full names of GMT's shared library.
"""
if env is None:
env = os.environ
libnames = clib_names(os_name=sys.platform) # e.g. libgmt.so, libgmt.dylib, gmt.dll
libpath = env.get("GMT_LIBRARY_PATH", "") # e.g. $HOME/miniconda/envs/pygmt/lib

lib_fullnames = [os.path.join(libpath, libname) for libname in libnames]
# Search for DLLs in PATH if GMT_LIBRARY_PATH is not defined [Windows only]
if not libpath and sys.platform == "win32":
for libname in libnames:
libfullpath = find_library(libname)
if libfullpath:
lib_fullnames.append(libfullpath)
return lib_fullnames


def check_libgmt(libgmt):
Expand Down
26 changes: 18 additions & 8 deletions pygmt/tests/test_clib_loading.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""
Test the functions that load libgmt
"""
import os
import pytest

from ..clib.loading import clib_name, load_libgmt, check_libgmt
from ..clib.loading import clib_names, load_libgmt, check_libgmt
from ..exceptions import GMTCLibError, GMTOSError, GMTCLibNotFoundError


Expand All @@ -20,16 +21,25 @@ def test_load_libgmt():

def test_load_libgmt_fail():
"Test that loading fails when given a bad library path."
env = {"GMT_LIBRARY_PATH": "not/a/real/path"}
# save the old value (if any) before setting a fake "GMT_LIBRARY_PATH"
old_gmt_library_path = os.environ.get("GMT_LIBRARY_PATH")

os.environ["GMT_LIBRARY_PATH"] = "/not/a/real/path"
with pytest.raises(GMTCLibNotFoundError):
load_libgmt(env=env)
load_libgmt()

# revert back to the original status (if any)
if old_gmt_library_path:
os.environ["GMT_LIBRARY_PATH"] = old_gmt_library_path
else:
del os.environ["GMT_LIBRARY_PATH"]


def test_clib_name():
def test_clib_names():
"Make sure we get the correct library name for different OS names"
for linux in ["linux", "linux2", "linux3"]:
assert clib_name(linux) == ["libgmt.so"]
assert clib_name("darwin") == ["libgmt.dylib"]
assert clib_name("win32") == ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
assert clib_names(linux) == ["libgmt.so"]
assert clib_names("darwin") == ["libgmt.dylib"]
assert clib_names("win32") == ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
with pytest.raises(GMTOSError):
clib_name("meh")
clib_names("meh")

0 comments on commit ce5de0e

Please sign in to comment.