From f11e1fe4382c6afb89e7116c832c43eeb3f1d1cc Mon Sep 17 00:00:00 2001 From: KhadeerBasha1232 <120262074+KhadeerBasha1232@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:39:25 +0530 Subject: [PATCH 1/2] Fix TopoJson non-collection geometries --- folium/features.py | 9 ++++++--- tests/test_features.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/folium/features.py b/folium/features.py index 65b6da9475..eaa4cd55d0 100644 --- a/folium/features.py +++ b/folium/features.py @@ -1053,9 +1053,12 @@ def recursive_get(data, keys): else: return data - geometries = recursive_get(self.data, self.object_path.split("."))[ - "geometries" - ] # noqa + geometry = recursive_get(self.data, self.object_path.split(".")) + geometries = ( + geometry["geometries"] + if geometry["type"] == "GeometryCollection" + else [geometry] + ) for feature in geometries: feature.setdefault("properties", {}).setdefault("style", {}).update( self.style_function(feature) diff --git a/tests/test_features.py b/tests/test_features.py index ecb5398f81..7b3c20ff06 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -17,6 +17,27 @@ from folium.utilities import JsCode +@pytest.mark.parametrize("geometry_type", ["Polygon", "MultiPolygon"]) +def test_topojson_non_collection_geometry(geometry_type): + """TopoJson supports objects that are not GeometryCollections.""" + topology = { + "type": "Topology", + "objects": { + "shape": { + "type": geometry_type, + "arcs": [[0]] if geometry_type == "Polygon" else [[[0]]], + } + }, + "arcs": [[[0, 0], [1, 0], [0, 1], [-1, -1]]], + "transform": {"scale": [1, 1], "translate": [0, 0]}, + } + + map_ = folium.Map() + folium.TopoJson(topology, "objects.shape").add_to(map_) + + assert "topojson.feature" in map_.get_root().render() + + @pytest.fixture def tmpl(): yield (""" From 7157e9bd6239b42a361f221c20d7093d06c99aae Mon Sep 17 00:00:00 2001 From: KhadeerBasha1232 <120262074+KhadeerBasha1232@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:46:17 +0530 Subject: [PATCH 2/2] Make TopoJson geometry lookup robust and fix second crash site in Popup/Tooltip --- folium/features.py | 25 ++++++++------------ tests/test_features.py | 52 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/folium/features.py b/folium/features.py index eaa4cd55d0..a11316791f 100644 --- a/folium/features.py +++ b/folium/features.py @@ -1047,6 +1047,14 @@ def __init__( def style_data(self) -> None: """Applies self.style_function to each feature of self.data.""" + for feature in self._get_geometries(): + feature.setdefault("properties", {}).setdefault("style", {}).update( + self.style_function(feature) + ) # noqa + + def _get_geometries(self) -> list: + """Return the selected TopoJSON object as a list of geometries.""" + def recursive_get(data, keys): if len(keys): return recursive_get(data.get(keys[0]), keys[1:]) @@ -1054,15 +1062,7 @@ def recursive_get(data, keys): return data geometry = recursive_get(self.data, self.object_path.split(".")) - geometries = ( - geometry["geometries"] - if geometry["type"] == "GeometryCollection" - else [geometry] - ) - for feature in geometries: - feature.setdefault("properties", {}).setdefault("style", {}).update( - self.style_function(feature) - ) # noqa + return geometry["geometries"] if "geometries" in geometry else [geometry] def render(self, **kwargs): """Renders the HTML representation of the element.""" @@ -1203,12 +1203,7 @@ def render(self, **kwargs): ) self.warn_for_geometry_collections() elif isinstance(self._parent, TopoJson): - obj_name = self._parent.object_path.split(".")[-1] - keys = tuple( - self._parent.data["objects"][obj_name]["geometries"][0][ - "properties" - ].keys() - ) + keys = tuple(self._parent._get_geometries()[0]["properties"].keys()) else: raise TypeError( f"You cannot add a {self._name} to anything other than a " diff --git a/tests/test_features.py b/tests/test_features.py index 7b3c20ff06..10a5ec6494 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -20,6 +20,7 @@ @pytest.mark.parametrize("geometry_type", ["Polygon", "MultiPolygon"]) def test_topojson_non_collection_geometry(geometry_type): """TopoJson supports objects that are not GeometryCollections.""" + style = {"color": "red"} topology = { "type": "Topology", "objects": { @@ -33,9 +34,56 @@ def test_topojson_non_collection_geometry(geometry_type): } map_ = folium.Map() - folium.TopoJson(topology, "objects.shape").add_to(map_) + folium.TopoJson( + topology, "objects.shape", style_function=lambda feature: style + ).add_to(map_) - assert "topojson.feature" in map_.get_root().render() + map_.get_root().render() + + assert topology["objects"]["shape"]["properties"]["style"] == style + + +def test_topojson_non_collection_geometry_without_type(): + """TopoJson does not require a type key to apply styles.""" + topology = { + "type": "Topology", + "objects": {"shape": {"arcs": [[0]]}}, + "arcs": [[[0, 0], [1, 0], [0, 1], [-1, -1]]], + "transform": {"scale": [1, 1], "translate": [0, 0]}, + } + + topojson = folium.TopoJson( + topology, + "objects.shape", + style_function=lambda feature: {"color": "red"}, + ) + + topojson.style_data() + + assert topology["objects"]["shape"]["properties"]["style"] == {"color": "red"} + + +@pytest.mark.parametrize("detail_type", [folium.GeoJsonPopup, folium.GeoJsonTooltip]) +def test_topojson_non_collection_geometry_detail(detail_type): + """TopoJson popup and tooltip fields support non-collection geometries.""" + topology = { + "type": "Topology", + "objects": { + "shape": { + "type": "Polygon", + "arcs": [[0]], + "properties": {"name": "A"}, + } + }, + "arcs": [[[0, 0], [1, 0], [0, 1], [-1, -1]]], + "transform": {"scale": [1, 1], "translate": [0, 0]}, + } + + map_ = folium.Map() + topojson = folium.TopoJson(topology, "objects.shape").add_to(map_) + detail_type(fields=["name"]).add_to(topojson) + + assert "name" in map_.get_root().render() @pytest.fixture