Repository: Toblerity/Shapely
Pull request #1183 (2021-08-24 19:43:15)

Deprecate setting of custom attributes on geometry objects
See https://github.com/Toblerity/Shapely/pull/1181 (and https://github.com/Toblerity/Shapely/pull/1180/files#r693378882). _If_ we want to disallow it for Shapely 2.0, this is adding a deprecation warning for it for 1.8

coveralls: 
[![Coverage Status](https://coveralls.io/builds/42381051/badge)](https://coveralls.io/builds/42381051)

Coverage increased (+0.05%) to 85.414% when pulling **342b8268b9cc552e01dd45424ca2dc07924a7d4e on jorisvandenbossche:deprecate-custom-attributes** into **822bf1c346d83fc8770659fdabd3385915b0ef8e on Toblerity:master**.

jorisvandenbossche: @sgillies @mwtoews you're fine with this additional deprecation? 
mwtoews: The only thing we can't warn against is users that extend geometry types:
```python
from shapely.geometry import Point

class MyPoint(Point):
    extra = 123

mp = MyPoint(2, 4)
print(mp.extra)  # 123
mp.extra = 456
print(mp.extra)  # 456
```
jorisvandenbossche: I suppose that in _theory_ we could also add a warning when subclassing the geometry classes (with a metaclass, see eg https://stackoverflow.com/questions/16056574/how-does-python-prevent-a-class-from-being-subclassed), but not directly sure how that impacts other things.
jorisvandenbossche: I am going to interpret the thumbs up of @sgillies at https://github.com/Toblerity/Shapely/pull/1181 as an approval of the general idea, and merge this.

Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 14db836..8f30851 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -73,6 +73,7 @@ version 2.0.0.
 - geometry .empty()
 - geometry .ctypes and .__array_interface__
 - multi-part geometry .__len__
+- setting custom attributes on geometry objects
 
 Geometry objects will become immutable in version 2.0.0.
 
diff --git a/docs/migration.rst b/docs/migration.rst
index ce80b49..217170c 100644
--- a/docs/migration.rst
+++ b/docs/migration.rst
@@ -68,9 +68,13 @@ Currently you can do::
     >>> line.name
     'my_geometry'
 
-This doesn't raise a warning in Shapely 1.8, but will start raising an
+In Shapely 1.8, this will start raising a warning, and will raise an
 AttributeError in Shapely 2.0.
 
+**How do I update my code?** There is no direct alternative for adding custom
+attributes to geometry objects. You can use other Python data structures such as
+(GeoJSON-like) dictionaries or GeoPandas' GeoDataFrames to store attributes
+alongside geometry features. 
 
 Multi-part geometries will no longer be "sequences" (length, iterable, indexable)
 =================================================================================
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index be5ff0c..f9866d9 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -14,6 +14,7 @@ import math
 import sys
 from warnings import warn
 from functools import wraps
+import warnings
 
 from shapely.affinity import affine_transform
 from shapely.coords import CoordinateSequence
@@ -241,6 +242,24 @@ class BaseGeometry:
         self._is_empty = val in [EMPTY, None]
         self.__geom__ = val
 
+    def __setattr__(self, name, value):
+        # first try regular attribute access via __getattribute__, so that
+        # our own (existing) attributes don't raise a warning
+        try:
+            object.__getattribute__(self, name)
+            super().__setattr__(name, value)
+            return
+        except AttributeError:
+            pass
+
+        # if custom atribute, raise the warning
+        warn(
+            "Setting custom attributes on geometry objects is deprecated, "
+            "and will raise an AttributeError in Shapely 2.0",
+            ShapelyDeprecationWarning, stacklevel=2
+        )
+        super().__setattr__(name, value)
+
     # Operators
     # ---------
 
diff --git a/shapely/geometry/proxy.py b/shapely/geometry/proxy.py
index 49de53a..f51d636 100644
--- a/shapely/geometry/proxy.py
+++ b/shapely/geometry/proxy.py
@@ -38,6 +38,11 @@ class CachingGeometryProxy:
     def gtag(self):
         return hash(repr(self.context))
 
+    def __setattr__(self, name, value):
+        # to override the custom one in BaseGeometry, so we don't warn
+        # for the proxy classes, which are already deprecated itself
+        object.__setattr__(self, name, value)
+
 
 class PolygonProxy(CachingGeometryProxy):
 


Test patch:
diff --git a/tests/test_geometry_base.py b/tests/test_geometry_base.py
index 9b4c68e..7c72079 100644
--- a/tests/test_geometry_base.py
+++ b/tests/test_geometry_base.py
@@ -1,4 +1,7 @@
 from shapely import geometry
+from shapely.errors import ShapelyDeprecationWarning
+
+import pytest
 
 
 def test_polygon():
@@ -15,3 +18,19 @@ def test_point():
 
 def test_geometry_collection():
     assert bool(geometry.GeometryCollection()) is False
+
+
+@pytest.mark.parametrize("geom", [
+    geometry.Point(1, 1),
+    geometry.LinearRing([(0, 0), (1, 1), (0, 1), (0, 0)]),
+    geometry.LineString([(0, 0), (1, 1), (0, 1), (0, 0)]),
+    geometry.Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]),
+    geometry.MultiPoint([(1, 1)]),
+    geometry.MultiLineString([[(0, 0), (1, 1), (0, 1), (0, 0)]]),
+    geometry.MultiPolygon([geometry.Polygon([(0, 0), (1, 1), (0, 1), (0, 0)])]),
+    geometry.GeometryCollection([geometry.Point(1, 1)]),
+])
+def test_setattr_disallowed(geom):
+    with pytest.warns(ShapelyDeprecationWarning, match="Setting custom attributes"):
+        geom.name = "test"
+    assert geom.name == "test"