diff --git a/folium/features.py b/folium/features.py index 587f5e053b..693059654c 100644 --- a/folium/features.py +++ b/folium/features.py @@ -1191,12 +1191,24 @@ def __init__( # noqa outside of type checking. self.style = style + def _geojson_features(self) -> list: + """Return the parent GeoJson's features, whatever form its data has.""" + assert isinstance(self._parent, GeoJson) + data = self._parent.data + if data.get("type") == "FeatureCollection": + return data["features"] + if "geometry" in data: + # A single Feature. + return [data] + # A bare geometry has no properties. + return [] + def warn_for_geometry_collections(self) -> None: """Checks for GeoJson GeometryCollection features to warn user about incompatibility.""" assert isinstance(self._parent, GeoJson) geom_collections = [ feature.get("properties") if feature.get("properties") is not None else key - for key, feature in enumerate(self._parent.data["features"]) + for key, feature in enumerate(self._geojson_features()) if feature["geometry"] and feature["geometry"]["type"] == "GeometryCollection" ] @@ -1212,10 +1224,9 @@ def render(self, **kwargs): """Renders the HTML representation of the element.""" figure = self.get_root() if isinstance(self._parent, GeoJson): + features = self._geojson_features() keys = tuple( - self._parent.data["features"][0]["properties"].keys() - if self._parent.data["features"] - else [] + (features[0].get("properties") or {}).keys() if features else [] ) self.warn_for_geometry_collections() elif isinstance(self._parent, TopoJson): diff --git a/tests/test_features.py b/tests/test_features.py index e41e0e8337..33adb8a397 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -86,6 +86,39 @@ def test_topojson_non_collection_geometry_detail(detail_type): assert "name" in map_.get_root().render() +@pytest.mark.parametrize("detail_type", [folium.GeoJsonPopup, folium.GeoJsonTooltip]) +@pytest.mark.parametrize( + "properties, fields", + [({"name": "A"}, ["name"]), (None, [])], + ids=["properties", "null-properties"], +) +def test_geojson_single_feature_detail(detail_type, properties, fields): + """GeoJson popup and tooltip support a single Feature, not only collections.""" + feature = { + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "properties": properties, + } + + map_ = folium.Map() + geojson = folium.GeoJson(feature).add_to(map_) + detail_type(fields=fields).add_to(geojson) + + rendered = map_.get_root().render() + assert all(field in rendered for field in fields) + + +@pytest.mark.parametrize("detail_type", [folium.GeoJsonPopup, folium.GeoJsonTooltip]) +def test_geojson_geometry_detail_unknown_field(detail_type): + """A bare geometry has no properties, so any field is reported as missing.""" + map_ = folium.Map() + geojson = folium.GeoJson({"type": "Point", "coordinates": [0, 0]}).add_to(map_) + detail_type(fields=["name"]).add_to(geojson) + + with pytest.raises(AssertionError, match="The field name is not available"): + map_.get_root().render() + + @pytest.fixture def tmpl(): yield ("""