|
1 | 1 | """Serializer tests.""" |
2 | 2 |
|
| 3 | +import json |
3 | 4 | import pytest |
| 5 | +import pandas as pd |
| 6 | +import geopandas as gpd |
| 7 | +from shapely.geometry import Point, Polygon |
4 | 8 | from keplergl.serializers import serialize_dataset |
5 | 9 |
|
6 | 10 |
|
7 | | -def test_serialize_dataframe(sample_df): |
8 | | - result = serialize_dataset(sample_df, "test") |
9 | | - assert result["id"] == "test" |
10 | | - assert result["format"] == "arrow" |
11 | | - assert "data" in result |
| 11 | +class TestDataFrameSerialization: |
| 12 | + """Tests for DataFrame serialization (from DataFrame.ipynb).""" |
12 | 13 |
|
| 14 | + def test_serialize_dataframe(self, sample_df): |
| 15 | + result = serialize_dataset(sample_df, "test") |
| 16 | + assert result["id"] == "test" |
| 17 | + assert result["format"] == "df" |
| 18 | + assert "data" in result |
13 | 19 |
|
14 | | -def test_serialize_geodataframe(sample_gdf): |
15 | | - result = serialize_dataset(sample_gdf, "test") |
16 | | - assert result["id"] == "test" |
17 | | - assert result["format"] == "geoarrow" |
18 | | - assert "data" in result |
| 20 | + def test_serialize_dataframe_with_cities(self): |
| 21 | + """Test DataFrame with city data (from DataFrame.ipynb).""" |
| 22 | + df = pd.DataFrame({ |
| 23 | + 'City': ['Buenos Aires', 'Brasilia', 'Santiago', 'Bogota', 'Caracas'], |
| 24 | + 'Country': ['Argentina', 'Brazil', 'Chile', 'Colombia', 'Venezuela'], |
| 25 | + 'Latitude': [-34.58, -15.78, -33.45, 4.60, 10.48], |
| 26 | + 'Longitude': [-58.66, -47.91, -70.66, -74.08, -66.86], |
| 27 | + 'Time': ['2019-09-01 08:00', '2019-09-01 09:00', '2019-09-01 10:00', |
| 28 | + '2019-09-01 11:00', '2019-09-01 12:00'], |
| 29 | + }) |
| 30 | + result = serialize_dataset(df, "data_1") |
| 31 | + assert result["id"] == "data_1" |
| 32 | + assert result["format"] == "df" |
| 33 | + assert result["data"]["columns"] == ['City', 'Country', 'Latitude', 'Longitude', 'Time'] |
| 34 | + assert len(result["data"]["data"]) == 5 |
19 | 35 |
|
| 36 | + def test_serialize_dataframe_with_hex_data(self): |
| 37 | + """Test DataFrame with H3 hex IDs and mixed types (from Load kepler.gl.ipynb).""" |
| 38 | + df = pd.DataFrame({ |
| 39 | + 'hex_id': ['89283082c2fffff', '8928308288fffff', '89283082c07ffff'], |
| 40 | + 'value': [64, 73, 65], |
| 41 | + 'is_true': [True, True, True], |
| 42 | + 'float_value': [64.1, 73.1, 65.1], |
| 43 | + 'empty': ['', '', ''], |
| 44 | + 'time': ['11/1/17 11:00', '11/1/17 11:00', '11/1/17 11:00'], |
| 45 | + }) |
| 46 | + result = serialize_dataset(df, "data_1") |
| 47 | + assert result["id"] == "data_1" |
| 48 | + assert result["format"] == "df" |
| 49 | + assert 'hex_id' in result["data"]["columns"] |
| 50 | + assert 'value' in result["data"]["columns"] |
| 51 | + assert 'is_true' in result["data"]["columns"] |
20 | 52 |
|
21 | | -def test_serialize_csv(): |
22 | | - csv_data = "lat,lng\n37.7749,-122.4194" |
23 | | - result = serialize_dataset(csv_data, "test") |
24 | | - assert result["format"] == "csv" |
25 | | - assert result["data"] == csv_data |
| 53 | + def test_serialize_dataframe_with_nan_filled(self): |
| 54 | + """Test DataFrame with NaN values filled with empty string.""" |
| 55 | + df = pd.DataFrame({ |
| 56 | + 'col1': [1, 2, 3], |
| 57 | + 'col2': ['a', None, 'c'], |
| 58 | + }) |
| 59 | + df = df.fillna('') |
| 60 | + result = serialize_dataset(df, "test") |
| 61 | + assert result["format"] == "df" |
| 62 | + assert result["data"]["data"][1][1] == '' |
26 | 63 |
|
27 | 64 |
|
28 | | -def test_serialize_geojson(): |
29 | | - geojson = {"type": "FeatureCollection", "features": []} |
30 | | - result = serialize_dataset(geojson, "test") |
31 | | - assert result["format"] == "geojson" |
32 | | - assert result["data"] == geojson |
| 65 | +class TestGeoDataFrameSerialization: |
| 66 | + """Tests for GeoDataFrame serialization (from GeoDataFrame.ipynb).""" |
| 67 | + |
| 68 | + def test_serialize_geodataframe(self, sample_gdf): |
| 69 | + result = serialize_dataset(sample_gdf, "test") |
| 70 | + assert result["id"] == "test" |
| 71 | + assert result["format"] == "geoarrow" |
| 72 | + assert "data" in result |
| 73 | + |
| 74 | + def test_serialize_geodataframe_with_timestamp(self): |
| 75 | + """Test GeoDataFrame with pd.Timestamp column. |
| 76 | +
|
| 77 | + Regression test for issue where GeoDataFrame containing Timestamp |
| 78 | + columns would fail during Arrow serialization. |
| 79 | + """ |
| 80 | + df = pd.DataFrame({ |
| 81 | + 'City': ['Buenos Aires'], |
| 82 | + 'Country': ['Argentina'], |
| 83 | + 'Latitude': [-34.58], |
| 84 | + 'Longitude': [-58.66], |
| 85 | + 'Timestamp': pd.Timestamp(2002, 3, 3), |
| 86 | + }) |
| 87 | + gdf = gpd.GeoDataFrame( |
| 88 | + df, |
| 89 | + geometry=gpd.points_from_xy(df.Longitude, df.Latitude), |
| 90 | + ) |
| 91 | + result = serialize_dataset(gdf, "cities") |
| 92 | + assert result["id"] == "cities" |
| 93 | + assert result["format"] == "geoarrow" |
| 94 | + assert "data" in result |
| 95 | + |
| 96 | + def test_serialize_geodataframe_points_from_xy(self): |
| 97 | + """Test GeoDataFrame created with points_from_xy (from GeoDataFrame.ipynb).""" |
| 98 | + df = pd.DataFrame({ |
| 99 | + 'City': ['Buenos Aires', 'Brasilia', 'Santiago', 'Bogota', 'Caracas'], |
| 100 | + 'Country': ['Argentina', 'Brazil', 'Chile', 'Colombia', 'Venezuela'], |
| 101 | + 'Latitude': [-34.58, -15.78, -33.45, 4.60, 10.48], |
| 102 | + 'Longitude': [-58.66, -47.91, -70.66, -74.08, -66.86], |
| 103 | + }) |
| 104 | + gdf = gpd.GeoDataFrame( |
| 105 | + df, |
| 106 | + geometry=gpd.points_from_xy(df.Longitude, df.Latitude), |
| 107 | + ) |
| 108 | + result = serialize_dataset(gdf, "cities") |
| 109 | + assert result["id"] == "cities" |
| 110 | + assert result["format"] == "geoarrow" |
| 111 | + assert "data" in result |
| 112 | + |
| 113 | + def test_serialize_geodataframe_with_polygons(self): |
| 114 | + """Test GeoDataFrame with Polygon geometries (like zipcode boundaries).""" |
| 115 | + gdf = gpd.GeoDataFrame({ |
| 116 | + 'ZIP_CODE': ['94107', '94105'], |
| 117 | + 'geometry': [ |
| 118 | + Polygon([(-122.40, 37.78), (-122.39, 37.78), |
| 119 | + (-122.39, 37.77), (-122.40, 37.77)]), |
| 120 | + Polygon([(-122.39, 37.79), (-122.38, 37.79), |
| 121 | + (-122.38, 37.78), (-122.39, 37.78)]), |
| 122 | + ], |
| 123 | + }) |
| 124 | + result = serialize_dataset(gdf, "zipcode") |
| 125 | + assert result["id"] == "zipcode" |
| 126 | + assert result["format"] == "geoarrow" |
| 127 | + assert "data" in result |
| 128 | + |
| 129 | + def test_serialize_geodataframe_with_crs(self): |
| 130 | + """Test GeoDataFrame with explicit CRS.""" |
| 131 | + gdf = gpd.GeoDataFrame( |
| 132 | + {"name": ["SF", "LA"]}, |
| 133 | + geometry=[Point(-122.4194, 37.7749), Point(-118.2437, 34.0522)], |
| 134 | + crs="EPSG:4326", |
| 135 | + ) |
| 136 | + result = serialize_dataset(gdf, "test") |
| 137 | + assert result["format"] == "geoarrow" |
| 138 | + assert "data" in result |
| 139 | + |
| 140 | + |
| 141 | +class TestGeoJSONSerialization: |
| 142 | + """Tests for GeoJSON serialization (from GeoJSON.ipynb).""" |
| 143 | + |
| 144 | + def test_serialize_geojson_dict(self): |
| 145 | + """Test GeoJSON as dict.""" |
| 146 | + geojson = {"type": "FeatureCollection", "features": []} |
| 147 | + result = serialize_dataset(geojson, "test") |
| 148 | + assert result["format"] == "geojson" |
| 149 | + assert result["data"] == geojson |
| 150 | + |
| 151 | + def test_serialize_geojson_feature_collection(self): |
| 152 | + """Test GeoJSON FeatureCollection with features.""" |
| 153 | + geojson = { |
| 154 | + "type": "FeatureCollection", |
| 155 | + "features": [ |
| 156 | + { |
| 157 | + "type": "Feature", |
| 158 | + "geometry": {"type": "Point", "coordinates": [-122.4, 37.8]}, |
| 159 | + "properties": {"name": "San Francisco"}, |
| 160 | + }, |
| 161 | + { |
| 162 | + "type": "Feature", |
| 163 | + "geometry": {"type": "Point", "coordinates": [-118.2, 34.0]}, |
| 164 | + "properties": {"name": "Los Angeles"}, |
| 165 | + }, |
| 166 | + ], |
| 167 | + } |
| 168 | + result = serialize_dataset(geojson, "geojson") |
| 169 | + assert result["id"] == "geojson" |
| 170 | + assert result["format"] == "geojson" |
| 171 | + assert result["data"]["type"] == "FeatureCollection" |
| 172 | + assert len(result["data"]["features"]) == 2 |
| 173 | + |
| 174 | + def test_serialize_geojson_string(self): |
| 175 | + """Test GeoJSON as string (from GeoJSON.ipynb - reading from file).""" |
| 176 | + geojson_str = json.dumps({ |
| 177 | + "type": "FeatureCollection", |
| 178 | + "features": [ |
| 179 | + { |
| 180 | + "type": "Feature", |
| 181 | + "geometry": {"type": "Point", "coordinates": [-122.4, 37.8]}, |
| 182 | + "properties": {"name": "Test"}, |
| 183 | + }, |
| 184 | + ], |
| 185 | + }) |
| 186 | + result = serialize_dataset(geojson_str, "geojson") |
| 187 | + assert result["id"] == "geojson" |
| 188 | + assert result["format"] == "geojson" |
| 189 | + assert result["data"]["type"] == "FeatureCollection" |
| 190 | + |
| 191 | + def test_serialize_geojson_polygon(self): |
| 192 | + """Test GeoJSON with Polygon geometry.""" |
| 193 | + geojson = { |
| 194 | + "type": "Feature", |
| 195 | + "geometry": { |
| 196 | + "type": "Polygon", |
| 197 | + "coordinates": [[ |
| 198 | + [-122.4, 37.8], [-122.3, 37.8], |
| 199 | + [-122.3, 37.7], [-122.4, 37.7], [-122.4, 37.8], |
| 200 | + ]], |
| 201 | + }, |
| 202 | + "properties": {"name": "Test Area"}, |
| 203 | + } |
| 204 | + result = serialize_dataset(geojson, "polygon") |
| 205 | + assert result["format"] == "geojson" |
| 206 | + |
| 207 | + |
| 208 | +class TestCSVSerialization: |
| 209 | + """Tests for CSV string serialization.""" |
| 210 | + |
| 211 | + def test_serialize_csv(self): |
| 212 | + csv_data = "lat,lng\n37.7749,-122.4194" |
| 213 | + result = serialize_dataset(csv_data, "test") |
| 214 | + assert result["format"] == "csv" |
| 215 | + assert result["data"] == csv_data |
| 216 | + |
| 217 | + def test_serialize_csv_multirow(self): |
| 218 | + """Test CSV with multiple rows.""" |
| 219 | + csv_data = "City,Latitude,Longitude\nSF,37.77,-122.42\nLA,34.05,-118.24" |
| 220 | + result = serialize_dataset(csv_data, "cities") |
| 221 | + assert result["id"] == "cities" |
| 222 | + assert result["format"] == "csv" |
| 223 | + assert result["data"] == csv_data |
| 224 | + |
| 225 | + |
| 226 | +class TestEdgeCases: |
| 227 | + """Tests for edge cases and error handling.""" |
| 228 | + |
| 229 | + def test_serialize_unsupported_type(self): |
| 230 | + """Test that unsupported types raise ValueError.""" |
| 231 | + with pytest.raises(ValueError, match="Unsupported data type"): |
| 232 | + serialize_dataset([1, 2, 3], "test") |
| 233 | + |
| 234 | + def test_serialize_empty_dataframe(self): |
| 235 | + """Test serializing empty DataFrame.""" |
| 236 | + df = pd.DataFrame({'col1': [], 'col2': []}) |
| 237 | + result = serialize_dataset(df, "empty") |
| 238 | + assert result["format"] == "df" |
| 239 | + assert result["data"]["columns"] == ['col1', 'col2'] |
| 240 | + assert result["data"]["data"] == [] |
| 241 | + |
| 242 | + def test_serialize_single_row_dataframe(self): |
| 243 | + """Test serializing single-row DataFrame.""" |
| 244 | + df = pd.DataFrame({'lat': [37.77], 'lng': [-122.42]}) |
| 245 | + result = serialize_dataset(df, "single") |
| 246 | + assert result["format"] == "df" |
| 247 | + assert len(result["data"]["data"]) == 1 |
| 248 | + |
| 249 | + def test_serialize_dataframe_with_various_dtypes(self): |
| 250 | + """Test DataFrame with various data types.""" |
| 251 | + df = pd.DataFrame({ |
| 252 | + 'int_col': [1, 2, 3], |
| 253 | + 'float_col': [1.1, 2.2, 3.3], |
| 254 | + 'str_col': ['a', 'b', 'c'], |
| 255 | + 'bool_col': [True, False, True], |
| 256 | + }) |
| 257 | + result = serialize_dataset(df, "mixed") |
| 258 | + assert result["format"] == "df" |
| 259 | + assert len(result["data"]["columns"]) == 4 |
0 commit comments