Skip to content

Commit

Permalink
Improve how PyGMT finds the GMT library (#702)
Browse files Browse the repository at this point in the history
Searches for all possible GMT library paths to load.
The paths from the highest priority to the lowest priority are:

1. the path defined by `GMT_LIBRARY_PATH`
2. the path returned by command `gmt --show-library`
3. On Windows, also check the path returned by `find_library` function
4. the standard system paths

* Check library names for FreeBSD
* Use generator yield in clib_full_names function
* Refactor test_load_libgmt_with_a_bad_library_path to use monkeypatch
* Monkeypatch test to check that GMTCLibNotFoundError is raised properly
* Check if the GMT shared library exists in GMT_LIBRARY_PATH

Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com>
  • Loading branch information
seisman and weiji14 authored Feb 12, 2021
1 parent d3e09c7 commit c869531
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 30 deletions.
52 changes: 35 additions & 17 deletions pygmt/clib/loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""
import ctypes
import os
import subprocess as sp
import sys
from ctypes.util import find_library

Expand All @@ -31,9 +32,10 @@ def load_libgmt():
If there was any problem loading the library (couldn't find it or
couldn't access the functions).
"""
lib_fullnames = clib_full_names()
lib_fullnames = []
error = True
for libname in lib_fullnames:
for libname in clib_full_names():
lib_fullnames.append(libname)
try:
libgmt = ctypes.CDLL(libname)
check_libgmt(libgmt)
Expand Down Expand Up @@ -72,7 +74,7 @@ def clib_names(os_name):
elif os_name.startswith("freebsd"): # FreeBSD
libnames = ["libgmt.so"]
else:
raise GMTOSError(f'Operating system "{sys.platform}" not supported.')
raise GMTOSError(f'Operating system "{os_name}" not supported.')
return libnames


Expand All @@ -86,24 +88,45 @@ def clib_full_names(env=None):
A dictionary containing the environment variables. If ``None``, will
default to ``os.environ``.
Returns
-------
Yields
------
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":
# list of libraries paths to search, sort by priority from high to low
# Search for libraries in GMT_LIBRARY_PATH if defined.
libpath = env.get("GMT_LIBRARY_PATH", "") # e.g. $HOME/miniconda/envs/pygmt/lib
if libpath:
for libname in libnames:
libfullpath = os.path.join(libpath, libname)
if os.path.exists(libfullpath):
yield libfullpath

# Search for the library returned by command "gmt --show-library"
try:
libfullpath = sp.check_output(
["gmt", "--show-library"], encoding="utf-8"
).rstrip("\n")
assert os.path.exists(libfullpath)
yield libfullpath
except (FileNotFoundError, AssertionError): # command not found
pass

# Search for DLLs in PATH (done by calling "find_library")
if sys.platform == "win32":
for libname in libnames:
libfullpath = find_library(libname)
if libfullpath:
lib_fullnames.append(libfullpath)
return lib_fullnames
yield libfullpath

# Search for library names in the system default path [the lowest priority]
for libname in libnames:
yield libname


def check_libgmt(libgmt):
Expand All @@ -128,10 +151,5 @@ def check_libgmt(libgmt):
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),
]
)
msg = f"Error loading libgmt. Couldn't access function GMT_{func}."
raise GMTCLibError(msg)
35 changes: 22 additions & 13 deletions pygmt/tests/test_clib_loading.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""
Test the functions that load libgmt.
"""
import os
import subprocess
import sys

import pytest
from pygmt.clib.loading import check_libgmt, clib_names, load_libgmt
Expand All @@ -23,22 +24,28 @@ def test_load_libgmt():
check_libgmt(load_libgmt())


def test_load_libgmt_fail():
@pytest.mark.skipif(sys.platform == "win32", reason="run on UNIX platforms only")
def test_load_libgmt_fails(monkeypatch):
"""
Test that loading fails when given a bad library path.
Test that GMTCLibNotFoundError is raised when GMT's shared library cannot
be found.
"""
# save the old value (if any) before setting a fake "GMT_LIBRARY_PATH"
old_gmt_library_path = os.environ.get("GMT_LIBRARY_PATH")
with monkeypatch.context() as mpatch:
mpatch.setattr(sys, "platform", "win32") # pretend to be on Windows
mpatch.setattr(
subprocess, "check_output", lambda cmd, encoding: "libfakegmt.so"
)
with pytest.raises(GMTCLibNotFoundError):
check_libgmt(load_libgmt())

os.environ["GMT_LIBRARY_PATH"] = "/not/a/real/path"
with pytest.raises(GMTCLibNotFoundError):
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_load_libgmt_with_a_bad_library_path(monkeypatch):
"""
Test that loading still works when given a bad library path.
"""
# Set a fake "GMT_LIBRARY_PATH"
monkeypatch.setenv("GMT_LIBRARY_PATH", "/not/a/real/path")
assert check_libgmt(load_libgmt()) is None


def test_clib_names():
Expand All @@ -49,5 +56,7 @@ def test_clib_names():
assert clib_names(linux) == ["libgmt.so"]
assert clib_names("darwin") == ["libgmt.dylib"]
assert clib_names("win32") == ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
for freebsd in ["freebsd10", "freebsd11", "freebsd12"]:
assert clib_names(freebsd) == ["libgmt.so"]
with pytest.raises(GMTOSError):
clib_names("meh")

0 comments on commit c869531

Please sign in to comment.