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,83 @@ 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
+ 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
+
1606
1690
def virtualfile_in ( # noqa: PLR0912
1607
1691
self ,
1608
1692
check_kind = None ,
@@ -1696,6 +1780,7 @@ def virtualfile_in( # noqa: PLR0912
1696
1780
"geojson" : tempfile_from_geojson ,
1697
1781
"grid" : self .virtualfile_from_grid ,
1698
1782
"image" : tempfile_from_image ,
1783
+ "stringio" : self .virtualfile_from_stringio ,
1699
1784
# Note: virtualfile_from_matrix is not used because a matrix can be
1700
1785
# converted to vectors instead, and using vectors allows for better
1701
1786
# handling of string type inputs (e.g. for datetime data types)
@@ -1704,7 +1789,7 @@ def virtualfile_in( # noqa: PLR0912
1704
1789
}[kind ]
1705
1790
1706
1791
# 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" }:
1708
1793
if kind == "image" and data .dtype != "uint8" :
1709
1794
msg = (
1710
1795
f"Input image has dtype: { data .dtype } which is unsupported, "
0 commit comments