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