Skip to content

Commit f3bc5c3

Browse files
authored
Let PyGMT work with the conda GMT package on Windows (#434)
This PR makes PyGMT work with the conda GMT package on Windows: - Fix how pygmt loads libraries on Windows - Separate out the libgmt loading tests - No longer need to install Ghostscript via chocolatey - Fix two failing tests on Windows - Two environmental variables are needed in Windows CI job: - GMT_LIBRARY_PATH: `C:\Miniconda\envs\testing\Library\bin` - GMT_SHAREDIR: `C:\Miniconda\envs\testing\Library\share\gmt`
1 parent c1f736a commit f3bc5c3

File tree

5 files changed

+68
-124
lines changed

5 files changed

+68
-124
lines changed

.azure-pipelines.yml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,15 +155,18 @@ jobs:
155155
########################################################################################
156156
- job:
157157
displayName: 'Windows'
158-
condition: False
159158

160159
pool:
161160
vmImage: 'vs2017-win2016'
162161

163162
variables:
164163
CONDA_REQUIREMENTS: requirements.txt
165164
CONDA_REQUIREMENTS_DEV: requirements-dev.txt
166-
CONDA_INSTALL_EXTRA: "codecov"
165+
CONDA_INSTALL_EXTRA: "codecov gmt=6.0.0"
166+
# ctypes.CDLL cannot find conda's libraries
167+
GMT_LIBRARY_PATH: 'C:\Miniconda\envs\testing\Library\bin'
168+
# Due to potential bug in GMT, we have to set GMT_SHAREDIR manually
169+
GMT_SHAREDIR: 'C:\Miniconda\envs\testing\Library\share\gmt'
167170

168171
strategy:
169172
matrix:
@@ -179,12 +182,6 @@ jobs:
179182

180183
steps:
181184

182-
# Install ghostscript separately since there is no Windows conda-forge package for it.
183-
- bash: |
184-
set -x -e
185-
choco install ghostscript
186-
displayName: Install ghostscript via chocolatey
187-
188185
- powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts"
189186
displayName: Add conda to PATH
190187

pygmt/clib/loading.py

Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -37,85 +37,48 @@ def load_libgmt(env=None):
3737
couldn't access the functions).
3838
3939
"""
40-
libpath = get_clib_path(env)
41-
try:
42-
libgmt = ctypes.CDLL(libpath)
43-
check_libgmt(libgmt)
44-
except OSError as err:
45-
msg = "\n".join(
46-
[
47-
"Error loading the GMT shared library '{}':".format(libpath),
48-
"{}".format(str(err)),
49-
]
50-
)
51-
raise GMTCLibNotFoundError(msg)
52-
return libgmt
53-
54-
55-
def get_clib_path(env):
56-
"""
57-
Get the path to the libgmt shared library.
58-
59-
Determine the file name and extension and append to the path set by
60-
``GMT_LIBRARY_PATH``, if any.
61-
62-
Parameters
63-
----------
64-
env : dict or None
65-
A dictionary containing the environment variables. If ``None``, will
66-
default to ``os.environ``.
67-
68-
Returns
69-
-------
70-
libpath : str
71-
The path to the libgmt shared library.
72-
73-
"""
74-
libname = clib_name()
7540
if env is None:
7641
env = os.environ
77-
if "GMT_LIBRARY_PATH" in env:
78-
libpath = os.path.join(env["GMT_LIBRARY_PATH"], libname)
79-
else:
80-
libpath = libname
81-
return libpath
42+
libnames = clib_name(os_name=sys.platform)
43+
libpath = env.get("GMT_LIBRARY_PATH", "")
44+
error = True
45+
for libname in libnames:
46+
try:
47+
libgmt = ctypes.CDLL(os.path.join(libpath, libname))
48+
check_libgmt(libgmt)
49+
error = False
50+
break
51+
except OSError as err:
52+
error = err
53+
if error:
54+
raise GMTCLibNotFoundError(
55+
"Error loading the GMT shared library '{}':".format(", ".join(libnames))
56+
)
57+
return libgmt
8258

8359

84-
def clib_name(os_name=None, is_64bit=None):
60+
def clib_name(os_name):
8561
"""
8662
Return the name of GMT's shared library for the current OS.
8763
8864
Parameters
8965
----------
90-
os_name : str or None
91-
The operating system name as given by ``sys.platform``
92-
(the default if None).
93-
is_64bit : bool or None
94-
Whether or not the OS is 64bit. Only used if the OS is ``win32``. If
95-
None, will determine automatically.
66+
os_name : str
67+
The operating system name as given by ``sys.platform``.
9668
9769
Returns
9870
-------
99-
libname : str
100-
The name of GMT's shared library.
71+
libname : list of str
72+
List of possible names of GMT's shared library.
10173
10274
"""
103-
if os_name is None:
104-
os_name = sys.platform
105-
106-
if is_64bit is None:
107-
is_64bit = sys.maxsize > 2 ** 32
108-
10975
if os_name.startswith("linux"):
110-
libname = "libgmt.so"
76+
libname = ["libgmt.so"]
11177
elif os_name == "darwin":
11278
# Darwin is macOS
113-
libname = "libgmt.dylib"
79+
libname = ["libgmt.dylib"]
11480
elif os_name == "win32":
115-
if is_64bit:
116-
libname = "gmt_w64.dll"
117-
else:
118-
libname = "gmt_w32.dll"
81+
libname = ["gmt.dll", "gmt_w64.dll", "gmt_w32.dll"]
11982
else:
12083
raise GMTOSError('Operating system "{}" not supported.'.format(sys.platform))
12184
return libname

pygmt/tests/test_clib.py

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,9 @@
1414

1515
from .. import clib
1616
from ..clib.session import FAMILIES, VIAS
17-
from ..clib.loading import clib_name, load_libgmt, check_libgmt, get_clib_path
1817
from ..clib.conversion import dataarray_to_matrix
1918
from ..exceptions import (
2019
GMTCLibError,
21-
GMTOSError,
22-
GMTCLibNotFoundError,
2320
GMTCLibNoSessionError,
2421
GMTInvalidInput,
2522
GMTVersionError,
@@ -70,54 +67,6 @@ def mock_get_libgmt_func(name, argtypes=None, restype=None):
7067
setattr(session, "get_libgmt_func", get_libgmt_func)
7168

7269

73-
def test_load_libgmt():
74-
"Test that loading libgmt works and doesn't crash."
75-
load_libgmt()
76-
77-
78-
def test_load_libgmt_fail():
79-
"Test that loading fails when given a bad library path."
80-
env = {"GMT_LIBRARY_PATH": "not/a/real/path"}
81-
with pytest.raises(GMTCLibNotFoundError):
82-
load_libgmt(env=env)
83-
84-
85-
def test_get_clib_path():
86-
"Test that the correct path is found when setting GMT_LIBRARY_PATH."
87-
# Get the real path to the library first
88-
with clib.Session() as lib:
89-
libpath = lib.info["library path"]
90-
libdir = os.path.dirname(libpath)
91-
# Assign it to the environment variable but keep a backup value to restore
92-
# later
93-
env = {"GMT_LIBRARY_PATH": libdir}
94-
95-
# Check that the path is determined correctly
96-
path_used = get_clib_path(env=env)
97-
assert os.path.samefile(path_used, libpath)
98-
assert os.path.dirname(path_used) == libdir
99-
100-
# Check that loading libgmt works
101-
load_libgmt(env=env)
102-
103-
104-
def test_check_libgmt():
105-
"Make sure check_libgmt fails when given a bogus library"
106-
with pytest.raises(GMTCLibError):
107-
check_libgmt(dict())
108-
109-
110-
def test_clib_name():
111-
"Make sure we get the correct library name for different OS names"
112-
for linux in ["linux", "linux2", "linux3"]:
113-
assert clib_name(linux) == "libgmt.so"
114-
assert clib_name("darwin") == "libgmt.dylib"
115-
assert clib_name("win32", is_64bit=True) == "gmt_w64.dll"
116-
assert clib_name("win32", is_64bit=False) == "gmt_w32.dll"
117-
with pytest.raises(GMTOSError):
118-
clib_name("meh")
119-
120-
12170
def test_getitem():
12271
"Test that I can get correct constants from the C lib"
12372
ses = clib.Session()

pygmt/tests/test_clib_loading.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Test the functions that load libgmt
3+
"""
4+
import pytest
5+
6+
from ..clib.loading import clib_name, load_libgmt, check_libgmt
7+
from ..exceptions import GMTCLibError, GMTOSError, GMTCLibNotFoundError
8+
9+
10+
def test_check_libgmt():
11+
"Make sure check_libgmt fails when given a bogus library"
12+
with pytest.raises(GMTCLibError):
13+
check_libgmt(dict())
14+
15+
16+
def test_load_libgmt():
17+
"Test that loading libgmt works and doesn't crash."
18+
check_libgmt(load_libgmt())
19+
20+
21+
def test_load_libgmt_fail():
22+
"Test that loading fails when given a bad library path."
23+
env = {"GMT_LIBRARY_PATH": "not/a/real/path"}
24+
with pytest.raises(GMTCLibNotFoundError):
25+
load_libgmt(env=env)
26+
27+
28+
def test_clib_name():
29+
"Make sure we get the correct library name for different OS names"
30+
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"]
34+
with pytest.raises(GMTOSError):
35+
clib_name("meh")

pygmt/tests/test_surface.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ def test_surface_with_outfile_param():
9090
)
9191
assert output is None # check that output is None since outfile is set
9292
assert os.path.exists(path=TEMP_GRID) # check that outfile exists at path
93-
grid = xr.open_dataarray(TEMP_GRID)
94-
assert isinstance(grid, xr.DataArray) # check that netcdf grid loaded properly
93+
with xr.open_dataarray(TEMP_GRID) as grid:
94+
assert isinstance(grid, xr.DataArray) # ensure netcdf grid loads ok
9595
finally:
9696
os.remove(path=TEMP_GRID)
9797
return output
@@ -108,8 +108,8 @@ def test_surface_short_aliases():
108108
output = surface(data=data, I="5m", R=[245, 255, 20, 30], G=TEMP_GRID)
109109
assert output is None # check that output is None since outfile is set
110110
assert os.path.exists(path=TEMP_GRID) # check that outfile exists at path
111-
grid = xr.open_dataarray(TEMP_GRID)
112-
assert isinstance(grid, xr.DataArray) # check that netcdf grid loaded properly
111+
with xr.open_dataarray(TEMP_GRID) as grid:
112+
assert isinstance(grid, xr.DataArray) # ensure netcdf grid loads ok
113113
finally:
114114
os.remove(path=TEMP_GRID)
115115
return output

0 commit comments

Comments
 (0)