diff --git a/pygmt/clib/loading.py b/pygmt/clib/loading.py index c98da792e82..ee46c9ebd5c 100644 --- a/pygmt/clib/loading.py +++ b/pygmt/clib/loading.py @@ -6,6 +6,7 @@ """ import ctypes import os +import subprocess as sp import sys from ctypes.util import find_library @@ -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) @@ -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 @@ -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): @@ -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) diff --git a/pygmt/tests/test_clib_loading.py b/pygmt/tests/test_clib_loading.py index 54fb89b3f5f..0d2edc9b60e 100644 --- a/pygmt/tests/test_clib_loading.py +++ b/pygmt/tests/test_clib_loading.py @@ -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 @@ -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(): @@ -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")