Skip to content

Commit 44b6e22

Browse files
authored
Merge branch 'api/virtualfile-from-stringio' into legend/stringio
2 parents ca50611 + deb917d commit 44b6e22

File tree

4 files changed

+102
-6
lines changed

4 files changed

+102
-6
lines changed

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,5 +317,6 @@ Low level access (these are mostly used by the :mod:`pygmt.clib` package):
317317
clib.Session.get_libgmt_func
318318
clib.Session.virtualfile_from_data
319319
clib.Session.virtualfile_from_grid
320+
clib.Session.virtualfile_from_stringio
320321
clib.Session.virtualfile_from_matrix
321322
clib.Session.virtualfile_from_vectors

pygmt/clib/session.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import contextlib
99
import ctypes as ctp
10+
import io
1011
import pathlib
1112
import sys
1213
import warnings
@@ -60,6 +61,7 @@
6061
"GMT_IS_PLP", # items could be any one of POINT, LINE, or POLY
6162
"GMT_IS_SURFACE", # items are 2-D grid
6263
"GMT_IS_VOLUME", # items are 3-D grid
64+
"GMT_IS_TEXT", # Text strings which triggers ASCII text reading
6365
]
6466

6567
METHODS = [
@@ -70,6 +72,11 @@
7072
DIRECTIONS = ["GMT_IN", "GMT_OUT"]
7173

7274
MODES = ["GMT_CONTAINER_ONLY", "GMT_IS_OUTPUT"]
75+
MODE_MODIFIERS = [
76+
"GMT_GRID_IS_CARTESIAN",
77+
"GMT_GRID_IS_GEO",
78+
"GMT_WITH_STRINGS",
79+
]
7380

7481
REGISTRATIONS = ["GMT_GRID_PIXEL_REG", "GMT_GRID_NODE_REG"]
7582

@@ -728,7 +735,7 @@ def create_data(
728735
mode_int = self._parse_constant(
729736
mode,
730737
valid=MODES,
731-
valid_modifiers=["GMT_GRID_IS_CARTESIAN", "GMT_GRID_IS_GEO"],
738+
valid_modifiers=MODE_MODIFIERS,
732739
)
733740
geometry_int = self._parse_constant(geometry, valid=GEOMETRIES)
734741
registration_int = self._parse_constant(registration, valid=REGISTRATIONS)
@@ -1603,6 +1610,83 @@ def virtualfile_from_grid(self, grid):
16031610
with self.open_virtualfile(*args) as vfile:
16041611
yield vfile
16051612

1613+
@contextlib.contextmanager
1614+
def virtualfile_from_stringio(self, stringio: io.StringIO):
1615+
r"""
1616+
Store a :class:`io.StringIO` object in a virtual file.
1617+
1618+
Store the contents of a :class:`io.StringIO` object in a GMT_DATASET container
1619+
and create a virtual file to pass to a GMT module.
1620+
1621+
Parameters
1622+
----------
1623+
stringio
1624+
The :class:`io.StringIO` object containing the data to be stored in the
1625+
virtual file.
1626+
1627+
Yields
1628+
------
1629+
fname
1630+
The name of the virtual file.
1631+
1632+
Examples
1633+
--------
1634+
>>> import io
1635+
>>> from pygmt.clib import Session
1636+
>>> stringio = io.StringIO(
1637+
... "# Comment\n"
1638+
... "H 24p Legend\n"
1639+
... "N 2\n"
1640+
... "S 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n"
1641+
... )
1642+
>>> with Session() as lib:
1643+
... with lib.virtualfile_from_stringio(stringio) as fin:
1644+
... lib.virtualfile_to_dataset(vfname=fin, output_type="pandas")
1645+
0
1646+
0 H 24p Legend
1647+
1 N 2
1648+
2 S 0.1i c 0.15i p300/12 0.25p 0.3i My circle
1649+
"""
1650+
# Parse the strings in the io.StringIO object.
1651+
# For simplicity, we make a few assumptions.
1652+
# - "#" indicates a comment line
1653+
# - ">" indicates a segment header
1654+
# - Only one table and one segment
1655+
header = None
1656+
string_arrays = []
1657+
for line in stringio.getvalue().splitlines():
1658+
if line.startswith("#"): # Skip comments
1659+
continue
1660+
if line.startswith(">"): # Segment header
1661+
if header is not None: # Only one segment is allowed now.
1662+
raise GMTInvalidInput("Only one segment is allowed.")
1663+
header = line
1664+
continue
1665+
string_arrays.append(line)
1666+
# Only one table and one segment. No numeric data, so n_columns is 0.
1667+
n_tables, n_segments, n_rows, n_columns = 1, 1, len(string_arrays), 0
1668+
1669+
family, geometry = "GMT_IS_DATASET", "GMT_IS_TEXT"
1670+
dataset = self.create_data(
1671+
family,
1672+
geometry,
1673+
mode="GMT_CONTAINER_ONLY|GMT_WITH_STRINGS",
1674+
dim=[n_tables, n_segments, n_rows, n_columns],
1675+
)
1676+
dataset = ctp.cast(dataset, ctp.POINTER(_GMT_DATASET))
1677+
# Assign the strings to the segment
1678+
seg = dataset.contents.table[0].contents.segment[0].contents
1679+
if header is not None:
1680+
seg.header = header.encode()
1681+
seg.text = strings_to_ctypes_array(string_arrays)
1682+
1683+
with self.open_virtualfile(family, geometry, "GMT_IN", dataset) as vfile:
1684+
try:
1685+
yield vfile
1686+
finally:
1687+
# Must set the text to None to avoid double freeing the memory
1688+
seg.text = None
1689+
16061690
def virtualfile_in( # noqa: PLR0912
16071691
self,
16081692
check_kind=None,
@@ -1696,6 +1780,7 @@ def virtualfile_in( # noqa: PLR0912
16961780
"geojson": tempfile_from_geojson,
16971781
"grid": self.virtualfile_from_grid,
16981782
"image": tempfile_from_image,
1783+
"stringio": self.virtualfile_from_stringio,
16991784
# Note: virtualfile_from_matrix is not used because a matrix can be
17001785
# converted to vectors instead, and using vectors allows for better
17011786
# handling of string type inputs (e.g. for datetime data types)
@@ -1704,7 +1789,7 @@ def virtualfile_in( # noqa: PLR0912
17041789
}[kind]
17051790

17061791
# Ensure the data is an iterable (Python list or tuple)
1707-
if kind in {"geojson", "grid", "image", "file", "arg"}:
1792+
if kind in {"geojson", "grid", "image", "file", "arg", "stringio"}:
17081793
if kind == "image" and data.dtype != "uint8":
17091794
msg = (
17101795
f"Input image has dtype: {data.dtype} which is unsupported, "

pygmt/helpers/utils.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Utilities and common tasks for wrapping the GMT modules.
33
"""
44

5+
import io
56
import os
67
import pathlib
78
import shutil
@@ -188,8 +189,10 @@ def _check_encoding(
188189

189190
def data_kind(
190191
data: Any = None, required: bool = True
191-
) -> Literal["arg", "file", "geojson", "grid", "image", "matrix", "vectors"]:
192-
"""
192+
) -> Literal[
193+
"arg", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors"
194+
]:
195+
r"""
193196
Check the kind of data that is provided to a module.
194197
195198
The ``data`` argument can be in any type, but only following types are supported:
@@ -222,6 +225,7 @@ def data_kind(
222225
>>> import numpy as np
223226
>>> import xarray as xr
224227
>>> import pathlib
228+
>>> import io
225229
>>> data_kind(data=None)
226230
'vectors'
227231
>>> data_kind(data=np.arange(10).reshape((5, 2)))
@@ -240,8 +244,12 @@ def data_kind(
240244
'grid'
241245
>>> data_kind(data=xr.DataArray(np.random.rand(3, 4, 5)))
242246
'image'
247+
>>> data_kind(data=io.StringIO("TEXT1\nTEXT23\n"))
248+
'stringio'
243249
"""
244-
kind: Literal["arg", "file", "geojson", "grid", "image", "matrix", "vectors"]
250+
kind: Literal[
251+
"arg", "file", "geojson", "grid", "image", "matrix", "stringio", "vectors"
252+
]
245253
if isinstance(data, str | pathlib.PurePath) or (
246254
isinstance(data, list | tuple)
247255
and all(isinstance(_file, str | pathlib.PurePath) for _file in data)
@@ -250,6 +258,8 @@ def data_kind(
250258
kind = "file"
251259
elif isinstance(data, bool | int | float) or (data is None and not required):
252260
kind = "arg"
261+
elif isinstance(data, io.StringIO):
262+
kind = "stringio"
253263
elif isinstance(data, xr.DataArray):
254264
kind = "image" if len(data.dims) == 3 else "grid"
255265
elif hasattr(data, "__geo_interface__"):

pygmt/src/legend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,4 @@ def legend(
9797

9898
with Session() as lib:
9999
with lib.virtualfile_in(data=spec, required_data=False) as vintbl:
100-
lib.call_module(module="legend", args=build_arg_list(kwargs, infile=vintbl))
100+
lib.call_module(module="legend", args=build_arg_list(kwargs, infile=vintbl))

0 commit comments

Comments
 (0)