Skip to content

Commit 2b054b6

Browse files
committed
Merge branch 'main' into refactor/data_kind
2 parents 2a6e788 + 86c9b2d commit 2b054b6

8 files changed

+519
-432
lines changed

pygmt/clib/session.py

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,41 +1203,46 @@ def write_data(self, family, geometry, mode, wesn, output, data):
12031203
raise GMTCLibError(f"Failed to write dataset to '{output}'")
12041204

12051205
@contextlib.contextmanager
1206-
def open_virtualfile(self, family, geometry, direction, data):
1206+
def open_virtualfile(
1207+
self,
1208+
family: str,
1209+
geometry: str,
1210+
direction: str,
1211+
data: ctp.c_void_p | None,
1212+
) -> Generator[str, None, None]:
12071213
"""
1208-
Open a GMT virtual file to pass data to and from a module.
1214+
Open a GMT virtual file associated with a data object for reading or writing.
12091215
1210-
GMT uses a virtual file scheme to pass in data or get data from API
1211-
modules. Use it to pass in your GMT data structure (created using
1212-
:meth:`pygmt.clib.Session.create_data`) to a module that expects an
1213-
input file, or get the output from a module that writes to a file.
1216+
GMT uses a virtual file scheme to pass in data or get data from API modules. Use
1217+
it to pass in your GMT data structure (created using
1218+
:meth:`pygmt.clib.Session.create_data`) to a module that expects an input file,
1219+
or get the output from a module that writes to a file.
12141220
1215-
Use in a ``with`` block. Will automatically close the virtual file when
1216-
leaving the ``with`` block. Because of this, no wrapper for
1217-
``GMT_Close_VirtualFile`` is provided.
1221+
Use in a ``with`` block. Will automatically close the virtual file when leaving
1222+
the ``with`` block. Because of this, no wrapper for ``GMT_Close_VirtualFile``
1223+
is provided.
12181224
12191225
Parameters
12201226
----------
1221-
family : str
1222-
A valid GMT data family name (e.g., ``"GMT_IS_DATASET"``). Should
1223-
be the same as the one you used to create your data structure.
1224-
geometry : str
1225-
A valid GMT data geometry name (e.g., ``"GMT_IS_POINT"``). Should
1226-
be the same as the one you used to create your data structure.
1227-
direction : str
1228-
Either ``"GMT_IN"`` or ``"GMT_OUT"`` to indicate if passing data to
1229-
GMT or getting it out of GMT, respectively.
1230-
By default, GMT can modify the data you pass in. Add modifier
1231-
``"GMT_IS_REFERENCE"`` to tell GMT the data are read-only, or
1232-
``"GMT_IS_DUPLICATE"`` to tell GMT to duplicate the data.
1233-
data : int or None
1234-
The ctypes void pointer to your GMT data structure. For output
1235-
(i.e., ``direction="GMT_OUT"``), it can be ``None`` to have GMT
1236-
automatically allocate the output GMT data structure.
1227+
family
1228+
A valid GMT data family name (e.g., ``"GMT_IS_DATASET"``). Should be the
1229+
same as the one you used to create your data structure.
1230+
geometry
1231+
A valid GMT data geometry name (e.g., ``"GMT_IS_POINT"``). Should be the
1232+
same as the one you used to create your data structure.
1233+
direction
1234+
Either ``"GMT_IN"`` or ``"GMT_OUT"`` to indicate if passing data to GMT or
1235+
getting it out of GMT, respectively. By default, GMT can modify the data you
1236+
pass in. Add modifier ``"GMT_IS_REFERENCE"`` to tell GMT the data are
1237+
read-only, or ``"GMT_IS_DUPLICATE"`` to tell GMT to duplicate the data.
1238+
data
1239+
The ctypes void pointer to the GMT data structure. For output (i.e.,
1240+
``direction="GMT_OUT"``), it can be ``None`` to have GMT automatically
1241+
allocate the output GMT data structure.
12371242
12381243
Yields
12391244
------
1240-
vfname : str
1245+
vfname
12411246
The name of the virtual file that you can pass to a GMT module.
12421247
12431248
Examples
@@ -1270,19 +1275,19 @@ def open_virtualfile(self, family, geometry, direction, data):
12701275
c_open_virtualfile = self.get_libgmt_func(
12711276
"GMT_Open_VirtualFile",
12721277
argtypes=[
1273-
ctp.c_void_p,
1274-
ctp.c_uint,
1275-
ctp.c_uint,
1276-
ctp.c_uint,
1277-
ctp.c_void_p,
1278-
ctp.c_char_p,
1278+
ctp.c_void_p, # V_API
1279+
ctp.c_uint, # family
1280+
ctp.c_uint, # geometry
1281+
ctp.c_uint, # direction
1282+
ctp.c_void_p, # data
1283+
ctp.c_char_p, # name
12791284
],
12801285
restype=ctp.c_int,
12811286
)
12821287

12831288
c_close_virtualfile = self.get_libgmt_func(
12841289
"GMT_Close_VirtualFile",
1285-
argtypes=[ctp.c_void_p, ctp.c_char_p],
1290+
argtypes=[ctp.c_void_p, ctp.c_char_p], # V_API, name
12861291
restype=ctp.c_int,
12871292
)
12881293

@@ -1297,19 +1302,24 @@ def open_virtualfile(self, family, geometry, direction, data):
12971302
self.session_pointer, family_int, geometry_int, direction_int, data, buff
12981303
)
12991304
if status != 0:
1300-
raise GMTCLibError("Failed to create a virtual file.")
1305+
msg = (
1306+
f"Failed to create a virtual file with {family=}, {geometry=}, "
1307+
f"{direction=}."
1308+
)
1309+
raise GMTCLibError(msg)
13011310

13021311
vfname = buff.value.decode()
13031312
try:
13041313
yield vfname
13051314
finally:
13061315
status = c_close_virtualfile(self.session_pointer, vfname.encode())
13071316
if status != 0:
1308-
raise GMTCLibError(f"Failed to close virtual file '{vfname}'.")
1317+
msg = f"Failed to close virtual file '{vfname}'."
1318+
raise GMTCLibError(msg)
13091319

13101320
def open_virtual_file(self, family, geometry, direction, data):
13111321
"""
1312-
Open a GMT virtual file to pass data to and from a module.
1322+
Open a GMT virtual file associated with a data object for reading or writing.
13131323
13141324
.. deprecated: 0.11.0
13151325

pygmt/datasets/load_remote_dataset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ def _load_remote_dataset(
441441
"returned unless only the pixel-registered grid is available."
442442
)
443443

444-
fname = f"@{prefix}_{resolution}_{registration[0]}"
444+
fname = f"@{prefix}_{resolution}_{registration[0]}" # type: ignore[index]
445445
if resinfo.tiled and region is None:
446446
raise GMTInvalidInput(
447447
f"'region' is required for {dataset.description} resolution '{resolution}'."
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""
2+
Test the Session.inquire_virtualfile method.
3+
"""
4+
5+
from pygmt import clib
6+
7+
8+
def test_inquire_virtualfile():
9+
"""
10+
Test that the inquire_virtualfile method returns the correct family.
11+
12+
Currently, only output virtual files are tested.
13+
"""
14+
with clib.Session() as lib:
15+
for family in [
16+
"GMT_IS_DATASET",
17+
"GMT_IS_DATASET|GMT_VIA_MATRIX",
18+
"GMT_IS_DATASET|GMT_VIA_VECTOR",
19+
]:
20+
with lib.open_virtualfile(
21+
family, "GMT_IS_PLP", "GMT_OUT|GMT_IS_REFERENCE", None
22+
) as vfile:
23+
assert lib.inquire_virtualfile(vfile) == lib["GMT_IS_DATASET"]
24+
25+
for family, geometry in [
26+
("GMT_IS_GRID", "GMT_IS_SURFACE"),
27+
("GMT_IS_IMAGE", "GMT_IS_SURFACE"),
28+
("GMT_IS_CUBE", "GMT_IS_VOLUME"),
29+
("GMT_IS_PALETTE", "GMT_IS_NONE"),
30+
("GMT_IS_POSTSCRIPT", "GMT_IS_NONE"),
31+
]:
32+
with lib.open_virtualfile(family, geometry, "GMT_OUT", None) as vfile:
33+
assert lib.inquire_virtualfile(vfile) == lib[family]
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
Test the Session.virtualfile_from_matrix method.
3+
"""
4+
5+
import numpy as np
6+
import pytest
7+
from pygmt import clib
8+
from pygmt.helpers import GMTTempFile
9+
10+
11+
@pytest.fixture(scope="module", name="dtypes")
12+
def fixture_dtypes():
13+
"""
14+
List of supported numpy dtypes.
15+
"""
16+
return "int8 int16 int32 int64 uint8 uint16 uint32 uint64 float32 float64".split()
17+
18+
19+
@pytest.mark.benchmark
20+
def test_virtualfile_from_matrix(dtypes):
21+
"""
22+
Test transforming a matrix to virtual file dataset.
23+
"""
24+
shape = (7, 5)
25+
for dtype in dtypes:
26+
data = np.arange(shape[0] * shape[1], dtype=dtype).reshape(shape)
27+
with clib.Session() as lib:
28+
with lib.virtualfile_from_matrix(data) as vfile:
29+
with GMTTempFile() as outfile:
30+
lib.call_module("info", [vfile, f"->{outfile.name}"])
31+
output = outfile.read(keep_tabs=True)
32+
bounds = "\t".join([f"<{col.min():.0f}/{col.max():.0f}>" for col in data.T])
33+
expected = f"<matrix memory>: N = {shape[0]}\t{bounds}\n"
34+
assert output == expected
35+
36+
37+
def test_virtualfile_from_matrix_slice(dtypes):
38+
"""
39+
Test transforming a slice of a larger array to virtual file dataset.
40+
"""
41+
shape = (10, 6)
42+
for dtype in dtypes:
43+
full_data = np.arange(shape[0] * shape[1], dtype=dtype).reshape(shape)
44+
rows = 5
45+
cols = 3
46+
data = full_data[:rows, :cols]
47+
with clib.Session() as lib:
48+
with lib.virtualfile_from_matrix(data) as vfile:
49+
with GMTTempFile() as outfile:
50+
lib.call_module("info", [vfile, f"->{outfile.name}"])
51+
output = outfile.read(keep_tabs=True)
52+
bounds = "\t".join([f"<{col.min():.0f}/{col.max():.0f}>" for col in data.T])
53+
expected = f"<matrix memory>: N = {rows}\t{bounds}\n"
54+
assert output == expected
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Test the Session.virtualfile_from_stringio method.
3+
"""
4+
5+
import io
6+
7+
import numpy as np
8+
from pygmt import clib
9+
10+
11+
def _stringio_to_dataset(data: io.StringIO):
12+
"""
13+
A helper function for check the virtualfile_from_stringio method.
14+
15+
The function does the following:
16+
17+
1. Creates a virtual file from the input StringIO object.
18+
2. Pass the virtual file to the ``read`` module, which reads the virtual file and
19+
writes it to another virtual file.
20+
3. Reads the output virtual file as a GMT_DATASET object.
21+
4. Extracts the header and the trailing text from the dataset and returns it as a
22+
string.
23+
"""
24+
with clib.Session() as lib:
25+
with (
26+
lib.virtualfile_from_stringio(data) as vintbl,
27+
lib.virtualfile_out(kind="dataset") as vouttbl,
28+
):
29+
lib.call_module("read", args=[vintbl, vouttbl, "-Td"])
30+
ds = lib.read_virtualfile(vouttbl, kind="dataset").contents
31+
32+
output = []
33+
table = ds.table[0].contents
34+
for segment in table.segment[: table.n_segments]:
35+
seg = segment.contents
36+
output.append(f"> {seg.header.decode()}" if seg.header else ">")
37+
output.extend(np.char.decode(seg.text[: seg.n_rows]))
38+
return "\n".join(output) + "\n"
39+
40+
41+
def test_virtualfile_from_stringio():
42+
"""
43+
Test the virtualfile_from_stringio method.
44+
"""
45+
data = io.StringIO(
46+
"# Comment\n"
47+
"H 24p Legend\n"
48+
"N 2\n"
49+
"S 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n"
50+
)
51+
expected = (
52+
">\n" "H 24p Legend\n" "N 2\n" "S 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n"
53+
)
54+
assert _stringio_to_dataset(data) == expected
55+
56+
57+
def test_one_segment():
58+
"""
59+
Test the virtualfile_from_stringio method with one segment.
60+
"""
61+
data = io.StringIO(
62+
"# Comment\n"
63+
"> Segment 1\n"
64+
"1 2 3 ABC\n"
65+
"4 5 DE\n"
66+
"6 7 8 9 FGHIJK LMN OPQ\n"
67+
"RSTUVWXYZ\n"
68+
)
69+
expected = (
70+
"> Segment 1\n"
71+
"1 2 3 ABC\n"
72+
"4 5 DE\n"
73+
"6 7 8 9 FGHIJK LMN OPQ\n"
74+
"RSTUVWXYZ\n"
75+
)
76+
assert _stringio_to_dataset(data) == expected
77+
78+
79+
def test_multiple_segments():
80+
"""
81+
Test the virtualfile_from_stringio method with multiple segments.
82+
"""
83+
data = io.StringIO(
84+
"# Comment line 1\n"
85+
"# Comment line 2\n"
86+
"> Segment 1\n"
87+
"1 2 3 ABC\n"
88+
"4 5 DE\n"
89+
"6 7 8 9 FG\n"
90+
"# Comment line 3\n"
91+
"> Segment 2\n"
92+
"1 2 3 ABC\n"
93+
"4 5 DE\n"
94+
"6 7 8 9 FG\n"
95+
)
96+
expected = (
97+
"> Segment 1\n"
98+
"1 2 3 ABC\n"
99+
"4 5 DE\n"
100+
"6 7 8 9 FG\n"
101+
"> Segment 2\n"
102+
"1 2 3 ABC\n"
103+
"4 5 DE\n"
104+
"6 7 8 9 FG\n"
105+
)
106+
assert _stringio_to_dataset(data) == expected

0 commit comments

Comments
 (0)