7
7
8
8
import contextlib
9
9
import ctypes as ctp
10
+ import io
10
11
import pathlib
11
12
import sys
12
13
import warnings
60
61
"GMT_IS_PLP" , # items could be any one of POINT, LINE, or POLY
61
62
"GMT_IS_SURFACE" , # items are 2-D grid
62
63
"GMT_IS_VOLUME" , # items are 3-D grid
64
+ "GMT_IS_TEXT" , # Text strings which triggers ASCII text reading
63
65
]
64
66
65
67
METHODS = [
70
72
DIRECTIONS = ["GMT_IN" , "GMT_OUT" ]
71
73
72
74
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
+ ]
73
80
74
81
REGISTRATIONS = ["GMT_GRID_PIXEL_REG" , "GMT_GRID_NODE_REG" ]
75
82
@@ -728,7 +735,7 @@ def create_data(
728
735
mode_int = self ._parse_constant (
729
736
mode ,
730
737
valid = MODES ,
731
- valid_modifiers = [ "GMT_GRID_IS_CARTESIAN" , "GMT_GRID_IS_GEO" ] ,
738
+ valid_modifiers = MODE_MODIFIERS ,
732
739
)
733
740
geometry_int = self ._parse_constant (geometry , valid = GEOMETRIES )
734
741
registration_int = self ._parse_constant (registration , valid = REGISTRATIONS )
@@ -1603,6 +1610,100 @@ def virtualfile_from_grid(self, grid):
1603
1610
with self .open_virtualfile (* args ) as vfile :
1604
1611
yield vfile
1605
1612
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
+ For simplicity, currently we make following assumptions in the StringIO object
1622
+
1623
+ - ``"#"`` indicates a comment line.
1624
+ - ``">"`` indicates a segment header.
1625
+
1626
+ Parameters
1627
+ ----------
1628
+ stringio
1629
+ The :class:`io.StringIO` object containing the data to be stored in the
1630
+ virtual file.
1631
+
1632
+ Yields
1633
+ ------
1634
+ fname
1635
+ The name of the virtual file.
1636
+
1637
+ Examples
1638
+ --------
1639
+ >>> import io
1640
+ >>> from pygmt.clib import Session
1641
+ >>> # A StringIO object containing legend specifications
1642
+ >>> stringio = io.StringIO(
1643
+ ... "# Comment\n"
1644
+ ... "H 24p Legend\n"
1645
+ ... "N 2\n"
1646
+ ... "S 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n"
1647
+ ... )
1648
+ >>> with Session() as lib:
1649
+ ... with lib.virtualfile_from_stringio(stringio) as fin:
1650
+ ... lib.virtualfile_to_dataset(vfname=fin, output_type="pandas")
1651
+ 0
1652
+ 0 H 24p Legend
1653
+ 1 N 2
1654
+ 2 S 0.1i c 0.15i p300/12 0.25p 0.3i My circle
1655
+ """
1656
+ # Parse the io.StringIO object.
1657
+ segments = []
1658
+ current_segment = {"header" : "" , "data" : []}
1659
+ for line in stringio .getvalue ().splitlines ():
1660
+ if line .startswith ("#" ): # Skip comments
1661
+ continue
1662
+ if line .startswith (">" ): # Segment header
1663
+ if current_segment ["data" ]: # If we have data, start a new segment
1664
+ segments .append (current_segment )
1665
+ current_segment = {"header" : "" , "data" : []}
1666
+ current_segment ["header" ] = line .strip (">" ).lstrip ()
1667
+ else :
1668
+ current_segment ["data" ].append (line ) # type: ignore[attr-defined]
1669
+ if current_segment ["data" ]: # Add the last segment if it has data
1670
+ segments .append (current_segment )
1671
+
1672
+ # One table with one or more segments.
1673
+ # n_rows is the maximum number of rows/records for all segments.
1674
+ # n_columns is the number of numeric data columns, so it's 0 here.
1675
+ n_tables = 1
1676
+ n_segments = len (segments )
1677
+ n_rows = max (len (segment ["data" ]) for segment in segments )
1678
+ n_columns = 0
1679
+
1680
+ # Create the GMT_DATASET container
1681
+ family , geometry = "GMT_IS_DATASET" , "GMT_IS_TEXT"
1682
+ dataset = self .create_data (
1683
+ family ,
1684
+ geometry ,
1685
+ mode = "GMT_CONTAINER_ONLY|GMT_WITH_STRINGS" ,
1686
+ dim = [n_tables , n_segments , n_rows , n_columns ],
1687
+ )
1688
+ dataset = ctp .cast (dataset , ctp .POINTER (_GMT_DATASET ))
1689
+ table = dataset .contents .table [0 ].contents
1690
+ for i , segment in enumerate (segments ):
1691
+ seg = table .segment [i ].contents
1692
+ if segment ["header" ]:
1693
+ seg .header = segment ["header" ].encode () # type: ignore[attr-defined]
1694
+ seg .text = strings_to_ctypes_array (segment ["data" ])
1695
+
1696
+ with self .open_virtualfile (family , geometry , "GMT_IN" , dataset ) as vfile :
1697
+ try :
1698
+ yield vfile
1699
+ finally :
1700
+ # Must set the pointers to None to avoid double freeing the memory.
1701
+ # Maybe upstream bug.
1702
+ for i in range (n_segments ):
1703
+ seg = table .segment [i ].contents
1704
+ seg .header = None
1705
+ seg .text = None
1706
+
1606
1707
def virtualfile_in ( # noqa: PLR0912
1607
1708
self ,
1608
1709
check_kind = None ,
@@ -1696,6 +1797,7 @@ def virtualfile_in( # noqa: PLR0912
1696
1797
"geojson" : tempfile_from_geojson ,
1697
1798
"grid" : self .virtualfile_from_grid ,
1698
1799
"image" : tempfile_from_image ,
1800
+ "stringio" : self .virtualfile_from_stringio ,
1699
1801
# Note: virtualfile_from_matrix is not used because a matrix can be
1700
1802
# converted to vectors instead, and using vectors allows for better
1701
1803
# handling of string type inputs (e.g. for datetime data types)
@@ -1704,7 +1806,7 @@ def virtualfile_in( # noqa: PLR0912
1704
1806
}[kind ]
1705
1807
1706
1808
# Ensure the data is an iterable (Python list or tuple)
1707
- if kind in {"geojson" , "grid" , "image" , "file" , "arg" }:
1809
+ if kind in {"geojson" , "grid" , "image" , "file" , "arg" , "stringio" }:
1708
1810
if kind == "image" and data .dtype != "uint8" :
1709
1811
msg = (
1710
1812
f"Input image has dtype: { data .dtype } which is unsupported, "
0 commit comments