Skip to content

Commit ce5de0e

Browse files
authored
Improve how PyGMT finds the GMT library (#440)
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
1 parent 6d4c695 commit ce5de0e

File tree

3 files changed

+63
-30
lines changed

3 files changed

+63
-30
lines changed

.azure-pipelines.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,6 @@ jobs:
179179
CONDA_REQUIREMENTS: requirements.txt
180180
CONDA_REQUIREMENTS_DEV: requirements-dev.txt
181181
CONDA_INSTALL_EXTRA: "codecov gmt=6.0.0"
182-
# ctypes.CDLL cannot find conda's libraries
183-
GMT_LIBRARY_PATH: 'C:\Miniconda\envs\testing\Library\bin'
184182

185183
strategy:
186184
matrix:

pygmt/clib/loading.py

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,19 @@
77
import os
88
import sys
99
import ctypes
10+
from ctypes.util import find_library
1011

1112
from ..exceptions import GMTOSError, GMTCLibError, GMTCLibNotFoundError
1213

1314

14-
def load_libgmt(env=None):
15+
def load_libgmt():
1516
"""
1617
Find and load ``libgmt`` as a :py:class:`ctypes.CDLL`.
1718
1819
By default, will look for the shared library in the directory specified by
1920
the environment variable ``GMT_LIBRARY_PATH``. If it's not set, will let
2021
ctypes try to find the library.
2122
22-
Parameters
23-
----------
24-
env : dict or None
25-
A dictionary containing the environment variables. If ``None``, will
26-
default to ``os.environ``.
27-
2823
Returns
2924
-------
3025
:py:class:`ctypes.CDLL` object
@@ -37,27 +32,26 @@ def load_libgmt(env=None):
3732
couldn't access the functions).
3833
3934
"""
40-
if env is None:
41-
env = os.environ
42-
libnames = clib_name(os_name=sys.platform)
43-
libpath = env.get("GMT_LIBRARY_PATH", "")
35+
lib_fullnames = clib_full_names()
4436
error = True
45-
for libname in libnames:
37+
for libname in lib_fullnames:
4638
try:
47-
libgmt = ctypes.CDLL(os.path.join(libpath, libname))
39+
libgmt = ctypes.CDLL(libname)
4840
check_libgmt(libgmt)
4941
error = False
5042
break
5143
except OSError as err:
5244
error = err
5345
if error:
5446
raise GMTCLibNotFoundError(
55-
"Error loading the GMT shared library '{}':".format(", ".join(libnames))
47+
"Error loading the GMT shared library '{}':".format(
48+
", ".join(lib_fullnames)
49+
)
5650
)
5751
return libgmt
5852

5953

60-
def clib_name(os_name):
54+
def clib_names(os_name):
6155
"""
6256
Return the name of GMT's shared library for the current OS.
6357
@@ -68,20 +62,51 @@ def clib_name(os_name):
6862
6963
Returns
7064
-------
71-
libname : list of str
65+
libnames : list of str
7266
List of possible names of GMT's shared library.
7367
7468
"""
7569
if os_name.startswith("linux"):
76-
libname = ["libgmt.so"]
70+
libnames = ["libgmt.so"]
7771
elif os_name == "darwin":
7872
# Darwin is macOS
79-
libname = ["libgmt.dylib"]
73+
libnames = ["libgmt.dylib"]
8074
elif os_name == "win32":
81-
libname = ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
75+
libnames = ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
8276
else:
8377
raise GMTOSError('Operating system "{}" not supported.'.format(sys.platform))
84-
return libname
78+
return libnames
79+
80+
81+
def clib_full_names(env=None):
82+
"""
83+
Return the full path of GMT's shared library for the current OS.
84+
85+
Parameters
86+
----------
87+
env : dict or None
88+
A dictionary containing the environment variables. If ``None``, will
89+
default to ``os.environ``.
90+
91+
Returns
92+
-------
93+
lib_fullnames: list of str
94+
List of possible full names of GMT's shared library.
95+
96+
"""
97+
if env is None:
98+
env = os.environ
99+
libnames = clib_names(os_name=sys.platform) # e.g. libgmt.so, libgmt.dylib, gmt.dll
100+
libpath = env.get("GMT_LIBRARY_PATH", "") # e.g. $HOME/miniconda/envs/pygmt/lib
101+
102+
lib_fullnames = [os.path.join(libpath, libname) for libname in libnames]
103+
# Search for DLLs in PATH if GMT_LIBRARY_PATH is not defined [Windows only]
104+
if not libpath and sys.platform == "win32":
105+
for libname in libnames:
106+
libfullpath = find_library(libname)
107+
if libfullpath:
108+
lib_fullnames.append(libfullpath)
109+
return lib_fullnames
85110

86111

87112
def check_libgmt(libgmt):

pygmt/tests/test_clib_loading.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""
22
Test the functions that load libgmt
33
"""
4+
import os
45
import pytest
56

6-
from ..clib.loading import clib_name, load_libgmt, check_libgmt
7+
from ..clib.loading import clib_names, load_libgmt, check_libgmt
78
from ..exceptions import GMTCLibError, GMTOSError, GMTCLibNotFoundError
89

910

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

2122
def test_load_libgmt_fail():
2223
"Test that loading fails when given a bad library path."
23-
env = {"GMT_LIBRARY_PATH": "not/a/real/path"}
24+
# save the old value (if any) before setting a fake "GMT_LIBRARY_PATH"
25+
old_gmt_library_path = os.environ.get("GMT_LIBRARY_PATH")
26+
27+
os.environ["GMT_LIBRARY_PATH"] = "/not/a/real/path"
2428
with pytest.raises(GMTCLibNotFoundError):
25-
load_libgmt(env=env)
29+
load_libgmt()
30+
31+
# revert back to the original status (if any)
32+
if old_gmt_library_path:
33+
os.environ["GMT_LIBRARY_PATH"] = old_gmt_library_path
34+
else:
35+
del os.environ["GMT_LIBRARY_PATH"]
2636

2737

28-
def test_clib_name():
38+
def test_clib_names():
2939
"Make sure we get the correct library name for different OS names"
3040
for linux in ["linux", "linux2", "linux3"]:
31-
assert clib_name(linux) == ["libgmt.so"]
32-
assert clib_name("darwin") == ["libgmt.dylib"]
33-
assert clib_name("win32") == ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
41+
assert clib_names(linux) == ["libgmt.so"]
42+
assert clib_names("darwin") == ["libgmt.dylib"]
43+
assert clib_names("win32") == ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
3444
with pytest.raises(GMTOSError):
35-
clib_name("meh")
45+
clib_names("meh")

0 commit comments

Comments
 (0)