77
88import contextlib
99import ctypes as ctp
10+ import io
1011import pathlib
1112import sys
1213import warnings
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
6567METHODS = [
7072DIRECTIONS = ["GMT_IN" , "GMT_OUT" ]
7173
7274MODES = ["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
7481REGISTRATIONS = ["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, "
0 commit comments