Skip to content

Commit 3258fc5

Browse files
authored
Merge branch 'main' into refactor/data_kind
2 parents 0c9f3eb + 3a589fa commit 3258fc5

File tree

8 files changed

+117
-54
lines changed

8 files changed

+117
-54
lines changed

.github/ISSUE_TEMPLATE/5-bump_gmt_checklist.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ using the following command:
3535
**To-Do for bumping the minimum required GMT version**:
3636

3737
- [ ] Bump the minimum required GMT version (1 PR)
38-
- [ ] Update `required_version` in `pygmt/clib/session.py`
38+
- [ ] Update `required_gmt_version` in `pygmt/clib/__init__.py`
3939
- [ ] Update `test_get_default` in `pygmt/tests/test_clib.py`
4040
- [ ] Update minimum required versions in `doc/minversions.md`
4141
- [ ] Remove unsupported GMT version from `.github/workflows/ci_tests_legacy.yaml`

doc/conf.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@
77

88
# ruff: isort: off
99
from sphinx_gallery.sorting import ExplicitOrder, ExampleTitleSortKey
10-
import pygmt
10+
from pygmt.clib import required_gmt_version
1111
from pygmt import __commit__, __version__
1212
from pygmt.sphinx_gallery import PyGMTScraper
1313

1414
# ruff: isort: on
1515

1616
requires_python = metadata("pygmt")["Requires-Python"]
17-
with pygmt.clib.Session() as lib:
18-
requires_gmt = f">={lib.required_version}"
17+
requires_gmt = f">={required_gmt_version}"
1918

2019
extensions = [
2120
"myst_parser",

pygmt/clib/__init__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@
55
interface. Access to the C library is done through ctypes.
66
"""
77

8-
from pygmt.clib.session import Session
8+
from packaging.version import Version
9+
from pygmt.clib.session import Session, __gmt_version__
10+
from pygmt.exceptions import GMTVersionError
911

10-
with Session() as lib:
11-
__gmt_version__ = lib.info["version"]
12+
required_gmt_version = "6.3.0"
13+
14+
# Check if the GMT version is older than the required version.
15+
if Version(__gmt_version__) < Version(required_gmt_version):
16+
msg = (
17+
f"Using an incompatible GMT version {__gmt_version__}. "
18+
f"Must be equal or newer than {required_gmt_version}."
19+
)
20+
raise GMTVersionError(msg)

pygmt/clib/loading.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,33 @@ def load_libgmt(lib_fullnames: Iterator[str] | None = None) -> ctypes.CDLL:
6464
return libgmt
6565

6666

67+
def get_gmt_version(libgmt: ctypes.CDLL) -> str:
68+
"""
69+
Get the GMT version string of the GMT shared library.
70+
71+
Parameters
72+
----------
73+
libgmt
74+
The GMT shared library.
75+
76+
Returns
77+
-------
78+
The GMT version string in *major.minor.patch* format.
79+
"""
80+
func = libgmt.GMT_Get_Version
81+
func.argtypes = (
82+
ctypes.c_void_p, # Unused parameter, so it can be None.
83+
ctypes.POINTER(ctypes.c_uint), # major
84+
ctypes.POINTER(ctypes.c_uint), # minor
85+
ctypes.POINTER(ctypes.c_uint), # patch
86+
)
87+
# The function return value is the current library version as a float, e.g., 6.5.
88+
func.restype = ctypes.c_float
89+
major, minor, patch = ctypes.c_uint(0), ctypes.c_uint(0), ctypes.c_uint(0)
90+
func(None, major, minor, patch)
91+
return f"{major.value}.{minor.value}.{patch.value}"
92+
93+
6794
def clib_names(os_name: str) -> list[str]:
6895
"""
6996
Return the name(s) of GMT's shared library for the current operating system.

pygmt/clib/session.py

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,9 @@
2424
strings_to_ctypes_array,
2525
vectors_to_arrays,
2626
)
27-
from pygmt.clib.loading import load_libgmt
27+
from pygmt.clib.loading import get_gmt_version, load_libgmt
2828
from pygmt.datatypes import _GMT_DATASET, _GMT_GRID, _GMT_IMAGE
29-
from pygmt.exceptions import (
30-
GMTCLibError,
31-
GMTCLibNoSessionError,
32-
GMTInvalidInput,
33-
GMTVersionError,
34-
)
29+
from pygmt.exceptions import GMTCLibError, GMTCLibNoSessionError, GMTInvalidInput
3530
from pygmt.helpers import (
3631
_validate_data_input,
3732
data_kind,
@@ -97,6 +92,7 @@
9792

9893
# Load the GMT library outside the Session class to avoid repeated loading.
9994
_libgmt = load_libgmt()
95+
__gmt_version__ = get_gmt_version(_libgmt)
10096

10197

10298
class Session:
@@ -154,9 +150,6 @@ class Session:
154150
-55 -47 -24 -10 190 981 1 1 8 14 1 1
155151
"""
156152

157-
# The minimum supported GMT version.
158-
required_version = "6.3.0"
159-
160153
@property
161154
def session_pointer(self):
162155
"""
@@ -211,27 +204,11 @@ def info(self):
211204

212205
def __enter__(self):
213206
"""
214-
Create a GMT API session and check the libgmt version.
207+
Create a GMT API session.
215208
216209
Calls :meth:`pygmt.clib.Session.create`.
217-
218-
Raises
219-
------
220-
GMTVersionError
221-
If the version reported by libgmt is less than
222-
``Session.required_version``. Will destroy the session before
223-
raising the exception.
224210
"""
225211
self.create("pygmt-session")
226-
# Need to store the version info because 'get_default' won't work after
227-
# the session is destroyed.
228-
version = self.info["version"]
229-
if Version(version) < Version(self.required_version):
230-
self.destroy()
231-
raise GMTVersionError(
232-
f"Using an incompatible GMT version {version}. "
233-
f"Must be equal or newer than {self.required_version}."
234-
)
235212
return self
236213

237214
def __exit__(self, exc_type, exc_value, traceback):

pygmt/tests/test_clib.py

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -577,27 +577,24 @@ def mock_defaults(api, name, value): # noqa: ARG001
577577
ses.destroy()
578578

579579

580-
def test_fails_for_wrong_version():
580+
def test_fails_for_wrong_version(monkeypatch):
581581
"""
582-
Make sure the clib.Session raises an exception if GMT is too old.
582+
Make sure that importing clib raise an exception if GMT is too old.
583583
"""
584+
import importlib
584585

585-
# Mock GMT_Get_Default to return an old version
586-
def mock_defaults(api, name, value): # noqa: ARG001
587-
"""
588-
Return an old version.
589-
"""
590-
if name == b"API_VERSION":
591-
value.value = b"5.4.3"
592-
else:
593-
value.value = b"bla"
594-
return 0
586+
with monkeypatch.context() as mpatch:
587+
# Make sure the current GMT major version is 6.
588+
assert clib.__gmt_version__.split(".")[0] == "6"
595589

596-
lib = clib.Session()
597-
with mock(lib, "GMT_Get_Default", mock_func=mock_defaults):
590+
# Monkeypatch the version string returned by pygmt.clib.loading.get_gmt_version.
591+
mpatch.setattr(clib.loading, "get_gmt_version", lambda libgmt: "5.4.3") # noqa: ARG005
592+
593+
# Reload clib.session and check the __gmt_version__ string.
594+
importlib.reload(clib.session)
595+
assert clib.session.__gmt_version__ == "5.4.3"
596+
597+
# Should raise an exception when pygmt.clib is loaded/reloaded.
598598
with pytest.raises(GMTVersionError):
599-
with lib:
600-
assert lib.info["version"] != "5.4.3"
601-
# Make sure the session is closed when the exception is raised.
602-
with pytest.raises(GMTCLibNoSessionError):
603-
assert lib.session_pointer
599+
importlib.reload(clib)
600+
assert clib.__gmt_version__ == "5.4.3" # Make sure it's still the old version

pygmt/tests/test_clib_loading.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@
1111
from pathlib import PurePath
1212

1313
import pytest
14-
from pygmt.clib.loading import check_libgmt, clib_full_names, clib_names, load_libgmt
14+
from pygmt.clib.loading import (
15+
check_libgmt,
16+
clib_full_names,
17+
clib_names,
18+
get_gmt_version,
19+
load_libgmt,
20+
)
1521
from pygmt.clib.session import Session
1622
from pygmt.exceptions import GMTCLibError, GMTCLibNotFoundError, GMTOSError
1723

@@ -360,3 +366,15 @@ def test_clib_full_names_gmt_library_path_incorrect_path_included(
360366
# Windows: find_library() searches the library in PATH, so one more
361367
npath = 2 if sys.platform == "win32" else 1
362368
assert list(lib_fullpaths) == [gmt_lib_realpath] * npath + gmt_lib_names
369+
370+
371+
###############################################################################
372+
# Test get_gmt_version
373+
def test_get_gmt_version():
374+
"""
375+
Test if get_gmt_version returns a version string in major.minor.patch format.
376+
"""
377+
version = get_gmt_version(load_libgmt())
378+
assert isinstance(version, str)
379+
assert len(version.split(".")) == 3 # In major.minor.patch format
380+
assert version.split(".")[0] == "6" # Is GMT 6.x.x

pygmt/tests/test_which.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
Test pygmt.which.
33
"""
44

5+
import os
6+
import sys
57
from pathlib import Path
8+
from tempfile import TemporaryDirectory
69

710
import pytest
811
from pygmt import which
912
from pygmt.helpers import unique_name
13+
from pygmt.session_management import begin, end
1014

1115

1216
def test_which():
@@ -40,3 +44,35 @@ def test_which_fails():
4044
which(bogus_file)
4145
with pytest.raises(FileNotFoundError):
4246
which(fname=[f"{bogus_file}.nc", f"{bogus_file}.txt"])
47+
48+
49+
@pytest.mark.skipif(
50+
sys.platform == "win32",
51+
reason="The Windows mkdir() function doesn't support multi-byte characters",
52+
)
53+
def test_which_nonascii_path(monkeypatch):
54+
"""
55+
Make sure PyGMT works with paths that contain non-ascii characters (e.g., Chinese).
56+
"""
57+
# Create a temporary directory with a Chinese suffix as a fake home directory.
58+
with TemporaryDirectory(suffix="中文") as fakehome:
59+
assert fakehome.endswith("中文") # Make sure fakename contains Chinese.
60+
(Path(fakehome) / ".gmt").mkdir() # Create the ~/.gmt directory.
61+
with monkeypatch.context() as mpatch:
62+
# Set HOME to the fake home directory and GMT will use it.
63+
mpatch.setenv("HOME", fakehome)
64+
# Check if HOME is set correctly
65+
assert os.getenv("HOME") == fakehome
66+
assert os.environ["HOME"] == fakehome
67+
68+
# Start a new session
69+
begin()
70+
# GMT should download the remote file under the new home directory.
71+
fname = which(fname="@static_earth_relief.nc", download="c", verbose="d")
72+
assert fname.startswith(fakehome)
73+
assert fname.endswith("static_earth_relief.nc")
74+
end()
75+
76+
# Make sure HOME is reverted correctly.
77+
assert os.getenv("HOME") != fakehome
78+
assert os.environ["HOME"] != fakehome

0 commit comments

Comments
 (0)