Repository: shapely/shapely (language: python)

Issue:
TODO: fix WKT for empty M and ZM geometries
On shapely `main` with #1808 to enable M and ZM in WKT outputs, one "TODO" item needs to be resolved for `test_repr_point_m_empty`. To demonstrate the issue:
```python
import shapely

assert shapely.geos_version[0:2] >= (3, 12), shapely.geos_version_string

empty_wkt = ["POINT Z EMPTY", "POINT M EMPTY", "POINT ZM EMPTY"]
empties = shapely.from_wkt(empty_wkt)

# This is one issue with repr
repr(empties)
# array([<POINT Z EMPTY>, <POINT Z EMPTY>, <POINT Z EMPTY>], dtype=object)
# but expected same as input wkt

# Another curious related issue
shapely.to_wkt(empties)
# array(['POINT Z EMPTY', None, None], dtype=object)

# But these really are different geometries
shapely.has_z(empties)
# array([ True, False,  True])
```

This is a shapely issue, and not with GEOS (3.12+). See `wkt_empty_3d_geometry` in `src/geos.c` for a possible resolution. Probably also rename away from "3D" too, since the third dimension is not always Z.


Discussion / hints:
> See `wkt_empty_3d_geometry` in `src/geos.c` for a possible resolution.

Should we just skip using that helper in case of GEOS 3.12+? Assuming that GEOS can now handle those cases correctly? (AFAIK that helper was added to correct some issues on the GEOS side, but it was also added in a time we didn't consider M values, so it's not surprising that it doesn't work correctly)
Agree on skipping helper for GEOS 3.12+. I have a PR ready after #2012 is merged (which resolves the "curious related issue" noted above).

Fix patch:
diff --git a/src/geos.c b/src/geos.c
index c7db029f9..8bbecca68 100644
--- a/src/geos.c
+++ b/src/geos.c
@@ -434,13 +434,13 @@ char check_to_wkt_trim_compatible(GEOSContextHandle_t ctx, const GEOSGeometry* g
 }
 #endif  // !GEOS_SINCE_3_13_0
 
-#if GEOS_SINCE_3_9_0
+#if GEOS_SINCE_3_9_0 && !GEOS_SINCE_3_12_0
 
 /* Checks whether the geometry is a 3D empty geometry and, if so, create the WKT string
  *
  * GEOS 3.9.* is able to distiguish 2D and 3D simple geometries (non-collections). But the
  * but the WKT serialization never writes a 3D empty geometry. This function fixes that.
- * It only makes sense to use this for GEOS versions >= 3.9.
+ * It only makes sense to use this for GEOS versions >= 3.9 && < 3.12.
  *
  * Pending GEOS ticket: https://trac.osgeo.org/geos/ticket/1129
  *
@@ -500,7 +500,7 @@ char wkt_empty_3d_geometry(GEOSContextHandle_t ctx, GEOSGeometry* geom, char** w
   return PGERR_SUCCESS;
 }
 
-#endif  // GEOS_SINCE_3_9_0
+#endif  // GEOS_SINCE_3_9_0 && !GEOS_SINCE_3_12_0
 
 /* GEOSInterpolate_r and GEOSInterpolateNormalized_r segfault on empty
  * geometries and also on collections with the first geometry empty.
diff --git a/src/geos.h b/src/geos.h
index 26367a1c8..6fbb68dc8 100644
--- a/src/geos.h
+++ b/src/geos.h
@@ -177,10 +177,10 @@ extern char check_to_wkt_compatible(GEOSContextHandle_t ctx, GEOSGeometry* geom)
 #if !GEOS_SINCE_3_13_0
 extern char check_to_wkt_trim_compatible(GEOSContextHandle_t ctx, const GEOSGeometry* geom, int dimension);
 #endif  // !GEOS_SINCE_3_13_0
-#if GEOS_SINCE_3_9_0
+#if GEOS_SINCE_3_9_0 && !GEOS_SINCE_3_12_0
 extern char wkt_empty_3d_geometry(GEOSContextHandle_t ctx, GEOSGeometry* geom,
                                   char** wkt);
-#endif  // GEOS_SINCE_3_9_0
+#endif  // GEOS_SINCE_3_9_0 && !GEOS_SINCE_3_12_0
 extern char geos_interpolate_checker(GEOSContextHandle_t ctx, GEOSGeometry* geom);
 
 extern int init_geos(PyObject* m);
diff --git a/src/pygeom.c b/src/pygeom.c
index 53bfdf6c9..2da75dcce 100644
--- a/src/pygeom.c
+++ b/src/pygeom.c
@@ -94,7 +94,14 @@ static PyObject* GeometryObject_ToWKT(GeometryObject* obj) {
   }
 #endif  // !GEOS_SINCE_3_13_0
 
-#if GEOS_SINCE_3_9_0
+#if !GEOS_SINCE_3_9_0
+  // Before GEOS 3.9.0, there was as segfault on e.g. MULTIPOINT (1 1, EMPTY)
+  errstate = check_to_wkt_compatible(ctx, geom);
+  if (errstate != PGERR_SUCCESS) {
+    goto finish;
+  }
+#elif !GEOS_SINCE_3_12_0
+  // Since GEOS 3.9.0 and before 3.12.0 further handling required
   errstate = wkt_empty_3d_geometry(ctx, geom, &wkt);
   if (errstate != PGERR_SUCCESS) {
     goto finish;
@@ -103,12 +110,6 @@ static PyObject* GeometryObject_ToWKT(GeometryObject* obj) {
     result = PyUnicode_FromString(wkt);
     goto finish;
   }
-#else
-  // Before GEOS 3.9.0, there was as segfault on e.g. MULTIPOINT (1 1, EMPTY)
-  errstate = check_to_wkt_compatible(ctx, geom);
-  if (errstate != PGERR_SUCCESS) {
-    goto finish;
-  }
 #endif
 
   GEOSWKTWriter* writer = GEOSWKTWriter_create_r(ctx);
diff --git a/src/ufuncs.c b/src/ufuncs.c
index 7d562db18..99d4e4534 100644
--- a/src/ufuncs.c
+++ b/src/ufuncs.c
@@ -3383,7 +3383,14 @@ static void to_wkt_func(char** args, const npy_intp* dimensions, const npy_intp*
         }
       }
 #endif  // !GEOS_SINCE_3_13_0
-#if GEOS_SINCE_3_9_0
+#if !GEOS_SINCE_3_9_0
+      // Before GEOS 3.9.0, there was as segfault on e.g. MULTIPOINT (1 1, EMPTY)
+      errstate = check_to_wkt_compatible(ctx, in1);
+      if (errstate != PGERR_SUCCESS) {
+        goto finish;
+      }
+#elif !GEOS_SINCE_3_12_0
+      // Since GEOS 3.9.0 and before 3.12.0 further handling required
       errstate = wkt_empty_3d_geometry(ctx, in1, &wkt);
       if (errstate != PGERR_SUCCESS) {
         goto finish;
@@ -3393,13 +3400,6 @@ static void to_wkt_func(char** args, const npy_intp* dimensions, const npy_intp*
         *out = PyUnicode_FromString(wkt);
         continue;
       }
-
-#else
-      // Before GEOS 3.9.0, there was as segfault on e.g. MULTIPOINT (1 1, EMPTY)
-      errstate = check_to_wkt_compatible(ctx, in1);
-      if (errstate != PGERR_SUCCESS) {
-        goto finish;
-      }
 #endif
       wkt = GEOSWKTWriter_write_r(ctx, writer, in1);
       if (wkt == NULL) {


Test patch:
diff --git a/shapely/tests/test_io.py b/shapely/tests/test_io.py
index d40ac7ad9..637e18a40 100644
--- a/shapely/tests/test_io.py
+++ b/shapely/tests/test_io.py
@@ -593,9 +593,6 @@ def test_repr_point_z_empty():
     assert repr(empty_point_z) == "<POINT Z EMPTY>"
 
 
-@pytest.mark.xfail(
-    reason="TODO: fix WKT for empty M and ZM geometries; see GH-2004", strict=True
-)
 @pytest.mark.skipif(
     shapely.geos_version < (3, 12, 0),
     reason="M coordinates not supported with GEOS < 3.12",