Skip to content

Commit c25cb04

Browse files
giswqsCopilot
andauthored
refactor: streamline geojson handling by centralizing file reading logic (#1336)
* refactor: streamline geojson handling by centralizing file reading logic Updated the handling of GeoJSON inputs across multiple files to utilize the new `geojson_to_gdf` function. This change improves code consistency and simplifies the process of reading GeoJSON data from both local files and URLs. Adjustments made in `common.py`, `foliumap.py`, `leafmap.py`, and `maplibregl.py` to replace direct calls to `gpd.read_file` with the new function. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * chore: update Python version in CI workflows to 3.13 Updated the Python version in multiple GitHub Actions workflows to 3.13, ensuring compatibility with the latest features and improvements. Changes made in docs.yml, installation.yml, macos.yml, ubuntu.yml, and windows.yml. * Address CodeRabbit review feedback - Remove redundant HTTP/file branching at three call sites in maplibregl.py (add_vector/add_geojson path, add_symbol, DateFilterWidget.__init__) since geojson_to_gdf() now internally branches on http URLs vs local files * Pin pyproj>=3.7 to fix Python 3.13 CI - uv was resolving the transitive pyproj (via geopandas) to 3.6.1, which has no cp313 wheel and fails to build from source (PROJ not found), breaking the 3.13 installation workflows on Linux/macOS/Windows - pyproj 3.7+ ships prebuilt cp313 wheels * Scope pyproj>=3.7 pin to Python 3.13 only - Unconditional pyproj>=3.7 conflicts with requires-python>=3.9 since pyproj 3.7+ requires Python>=3.10, making resolution unsatisfiable for 3.9-3.12 - Add an environment marker so the pin only applies on Python 3.13, where the missing cp313 wheel for pyproj 3.6.1 is the actual problem * Fix Python 3.13 CI: run installed venv instead of re-resolving with uv run The pyproj>=3.7 pin (reverted here) could not work: leafmap declares requires-python>=3.9, and `uv run` resolves the whole version range at once (universal resolution), so pyproj is forced to 3.6.1 (the only version that supports 3.9) — which has no cp313 wheel and fails to build from source. Instead, `uv pip install .` already resolves per-interpreter and correctly installs pyproj 3.7.x (with a cp313 wheel) on Python 3.13. The failure was only the subsequent `uv run` step re-resolving universally. Run the installed .venv's python/pytest directly so no re-resolution happens. - Remove pyproj pin from requirements.txt - installation/macos/windows/ubuntu workflows: replace `uv run` in the Test import and pytest steps with the .venv interpreter * Keep uv run for ubuntu pytest workflow Reverting ubuntu.yml to `uv run`: switching to the per-interpreter .venv pulls in numpy>=2.1, which breaks map JSON serialization in the test suite (29 tests fail with "ndarray is not JSON serializable"). The install-only workflows (installation/macos/windows) keep the .venv fix since they only do an import check and are unaffected. * Fix numpy>=2.1 GeoJSON serialization, support Python 3.14, add 3.14 to CI Root-cause fixes for the CI failures and Python 3.14 support: - sanitize_geojson (common.py): recursively convert NumPy arrays/scalars in GeoJSON to native Python types. Newer geopandas/numpy read list-valued feature properties (e.g. a "coordinates" property) back as ndarrays, which are not JSON serializable and broke ipyleaflet/folium widget serialization ("Object of type ndarray is not JSON serializable") across ~30 tests. Applied in Map.add_geojson for both leafmap.py and foliumap.py. - fit_bounds (leafmap.py): override ipyleaflet's fit_bounds to ensure an asyncio event loop exists first. Python 3.14 no longer provides an implicit current loop outside a running loop, so ipyleaflet's asyncio.ensure_future(self._fit_bounds(...)) raised RuntimeError. - Make fiona optional (leafmap.py, foliumap.py, common.py): fiona has no Python 3.14 wheel. It was only used to register the KML driver; the default pyogrio engine reads KML natively, so import it lazily and fall back. - requirements_dev.txt: fiona only on Python < 3.14 (no cp314 wheel yet). - CI: add Python 3.14 to the ubuntu/macOS/windows/installation matrices. ubuntu.yml now runs the installed .venv interpreter for the import check and pytest instead of `uv run`, which re-resolves universally under requires-python>=3.9 and pins pyproj to 3.6.1 (no cp313/cp314 wheel). Verified locally: full test suite (80 tests) passes on Python 3.14. * Remove stray us_states.kml test artifact * Use latest uv in CI so Python 3.14 can be installed The pinned uv 0.4.16 predates Python 3.14 and has no download for it ("No download found for request: cpython-3.14-..."). Bump the setup-uv version to "latest" in the workflows that test 3.14. * Bump docs-build CI to Python 3.13 docs.yml already uses 3.13; align docs-build.yml (was 3.12). * Fix docs CI on Python 3.13: run installed venv instead of uv run Bumping docs-build to 3.13 exposed the same universal-resolution trap: `uv run` re-resolves under requires-python>=3.9 and pins pyproj to 3.6.1, which has no cp313 wheel and fails to build (proj not found). Use the installed .venv interpreter for the import, pytest, and mkdocs steps in both docs.yml and docs-build.yml. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent beeabc4 commit c25cb04

11 files changed

Lines changed: 140 additions & 54 deletions

File tree

.github/workflows/docs-build.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
strategy:
1212
matrix:
13-
python-version: ["3.12"]
13+
python-version: ["3.13"]
1414

1515
env:
1616
PLANET_API_KEY: ${{ secrets.PLANET_API_KEY }}
@@ -51,18 +51,18 @@ jobs:
5151
5252
- name: Test import
5353
run: |
54-
uv run python -c "import leafmap; print('leafmap import successful')"
55-
# uv run python -c "from osgeo import gdal; print('gdal import successful')"
56-
# uv run gdalinfo --version
54+
.venv/bin/python -c "import leafmap; print('leafmap import successful')"
55+
# .venv/bin/python -c "from osgeo import gdal; print('gdal import successful')"
56+
# .venv/bin/gdalinfo --version
5757

5858
- name: Running pytest
5959
run: |
60-
uv run pytest . --verbose
60+
.venv/bin/pytest . --verbose
6161
6262
- name: Install mkdocs
6363
run: |
6464
uv pip install -r requirements_docs.txt
65-
uv run mkdocs build
65+
.venv/bin/mkdocs build
6666
6767
- name: Deploy to Netlify
6868
uses: nwtgck/actions-netlify@v4.0

.github/workflows/docs.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
strategy:
1212
matrix:
13-
python-version: ["3.12"]
13+
python-version: ["3.13"]
1414

1515
env:
1616
PLANET_API_KEY: ${{ secrets.PLANET_API_KEY }}
@@ -50,15 +50,15 @@ jobs:
5050
5151
- name: Test import
5252
run: |
53-
uv run python -c "import leafmap; print('leafmap import successful')"
54-
# uv run python -c "from osgeo import gdal; print('gdal import successful')"
55-
# uv run gdalinfo --version
53+
.venv/bin/python -c "import leafmap; print('leafmap import successful')"
54+
# .venv/bin/python -c "from osgeo import gdal; print('gdal import successful')"
55+
# .venv/bin/gdalinfo --version
5656

5757
- name: Running pytest
5858
run: |
59-
uv run pytest . --verbose
59+
.venv/bin/pytest . --verbose
6060
6161
- name: Install mkdocs
6262
run: |
6363
uv pip install -r requirements_docs.txt
64-
uv run mkdocs gh-deploy --force
64+
.venv/bin/mkdocs gh-deploy --force

.github/workflows/installation.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
python-version: ["3.12"]
16+
python-version: ["3.13", "3.14"]
1717
steps:
1818
- uses: actions/checkout@v7
1919

2020
- name: Install uv
2121
uses: astral-sh/setup-uv@v7
2222
with:
23-
version: "0.4.16"
23+
version: "latest"
2424
# enable-cache: true
2525

2626
- name: Set up Python ${{ matrix.python-version }}
@@ -32,4 +32,4 @@ jobs:
3232
uv pip install .
3333
3434
- name: Test import
35-
run: uv run python -c "import leafmap; print('leafmap import successful')"
35+
run: .venv/bin/python -c "import leafmap; print('leafmap import successful')"

.github/workflows/macos.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
config:
18-
- { os: macOS-latest, py: "3.12" }
18+
- { os: macOS-latest, py: "3.13" }
19+
- { os: macOS-latest, py: "3.14" }
1920
env:
2021
SDKROOT: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
2122
steps:
@@ -24,7 +25,7 @@ jobs:
2425
- name: Install uv
2526
uses: astral-sh/setup-uv@v7
2627
with:
27-
version: "0.4.16"
28+
version: "latest"
2829
# enable-cache: true
2930

3031
- name: Set up Python ${{ matrix.config.py }}
@@ -37,4 +38,4 @@ jobs:
3738
3839
- name: Test import
3940
run: |
40-
uv run python -c "import leafmap; print('leafmap import successful')"
41+
.venv/bin/python -c "import leafmap; print('leafmap import successful')"

.github/workflows/ubuntu.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
python-version: ["3.9", "3.10", "3.11", "3.12"]
16+
python-version: ["3.11", "3.12", "3.13", "3.14"]
1717

1818
env:
1919
PLANET_API_KEY: ${{ secrets.PLANET_API_KEY }}
@@ -33,7 +33,7 @@ jobs:
3333
- name: Install uv
3434
uses: astral-sh/setup-uv@v7
3535
with:
36-
version: "0.4.16"
36+
version: "latest"
3737
# enable-cache: true
3838

3939
- name: Set up Python ${{ matrix.python-version }}
@@ -52,10 +52,10 @@ jobs:
5252
5353
- name: Test import
5454
run: |
55-
uv run python -c "import leafmap; print('leafmap import successful')"
56-
# uv run python -c "from osgeo import gdal; print('gdal import successful')"
57-
# uv run gdalinfo --version
55+
.venv/bin/python -c "import leafmap; print('leafmap import successful')"
56+
# .venv/bin/python -c "from osgeo import gdal; print('gdal import successful')"
57+
# .venv/bin/gdalinfo --version
5858

5959
- name: Running pytest
6060
run: |
61-
uv run pytest . --verbose
61+
.venv/bin/pytest . --verbose

.github/workflows/windows.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: windows-latest
1313
strategy:
1414
matrix:
15-
python-version: ["3.12"]
15+
python-version: ["3.13", "3.14"]
1616

1717
env:
1818
PLANET_API_KEY: ${{ secrets.PLANET_API_KEY }}
@@ -29,7 +29,7 @@ jobs:
2929
- name: Install uv
3030
uses: astral-sh/setup-uv@v7
3131
with:
32-
version: "0.4.16"
32+
version: "latest"
3333
# enable-cache: true
3434

3535
- name: Set up Python ${{ matrix.python-version }}
@@ -42,4 +42,4 @@ jobs:
4242
4343
- name: Test import
4444
run: |
45-
uv run python -c "import leafmap; print('leafmap import successful')"
45+
.venv\Scripts\python -c "import leafmap; print('leafmap import successful')"

leafmap/common.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,11 +1516,15 @@ def kml_to_shp(in_kml, out_shp):
15161516

15171517
check_package(name="geopandas", URL="https://geopandas.org")
15181518

1519-
import fiona
15201519
import geopandas as gpd
15211520

1522-
# print(fiona.supported_drivers)
1523-
fiona.drvsupport.supported_drivers["KML"] = "rw"
1521+
try:
1522+
import fiona
1523+
1524+
fiona.drvsupport.supported_drivers["KML"] = "rw"
1525+
except ImportError:
1526+
# fiona is optional; the default pyogrio engine reads KML natively.
1527+
pass
15241528
df = gpd.read_file(in_kml, driver="KML")
15251529
df.to_file(out_shp)
15261530

@@ -1555,12 +1559,15 @@ def kml_to_geojson(in_kml, out_geojson=None):
15551559

15561560
check_package(name="geopandas", URL="https://geopandas.org")
15571561

1558-
import fiona
15591562
import geopandas as gpd
15601563

1561-
# import fiona
1562-
# print(fiona.supported_drivers)
1563-
fiona.drvsupport.supported_drivers["KML"] = "rw"
1564+
try:
1565+
import fiona
1566+
1567+
fiona.drvsupport.supported_drivers["KML"] = "rw"
1568+
except ImportError:
1569+
# fiona is optional; the default pyogrio engine reads KML natively.
1570+
pass
15641571
gdf = gpd.read_file(in_kml, driver="KML")
15651572

15661573
if out_geojson is not None:
@@ -1693,7 +1700,6 @@ def vector_to_geojson(
16931700

16941701
warnings.filterwarnings("ignore")
16951702
check_package(name="geopandas", URL="https://geopandas.org")
1696-
import fiona
16971703
import geopandas as gpd
16981704

16991705
if not filename.startswith("http"):
@@ -1702,7 +1708,13 @@ def vector_to_geojson(
17021708
filename = "zip://" + filename
17031709
ext = os.path.splitext(filename)[1].lower()
17041710
if ext == ".kml":
1705-
fiona.drvsupport.supported_drivers["KML"] = "rw"
1711+
try:
1712+
import fiona
1713+
1714+
fiona.drvsupport.supported_drivers["KML"] = "rw"
1715+
except ImportError:
1716+
# fiona is optional; the default pyogrio engine reads KML natively.
1717+
pass
17061718
df = gpd.read_file(
17071719
filename,
17081720
bbox=bbox,
@@ -3950,11 +3962,47 @@ def geojson_to_gdf(in_geojson, encoding="utf-8", **kwargs: Any):
39503962
with open(out_file, "w") as f:
39513963
json.dump(in_geojson, f)
39523964
in_geojson = out_file
3965+
elif isinstance(in_geojson, str) and in_geojson.startswith("http"):
3966+
try:
3967+
return gpd.read_file(in_geojson, encoding=encoding, **kwargs)
3968+
except Exception:
3969+
response = requests.get(in_geojson, timeout=30)
3970+
response.raise_for_status()
3971+
data = response.json()
3972+
if isinstance(data, dict) and data.get("type") == "Feature":
3973+
return gpd.GeoDataFrame.from_features([data], **kwargs)
3974+
return gpd.GeoDataFrame.from_features(data, **kwargs)
39533975

39543976
gdf = gpd.read_file(in_geojson, encoding=encoding, **kwargs)
39553977
return gdf
39563978

39573979

3980+
def sanitize_geojson(obj: Any) -> Any:
3981+
"""Recursively converts NumPy types in a GeoJSON-like object to native Python types.
3982+
3983+
GeoDataFrame.__geo_interface__ can leave NumPy arrays/scalars in feature
3984+
properties (e.g. list-valued columns are read back as ndarrays), which are
3985+
not JSON serializable and break widget serialization. This makes the object
3986+
safe for ``json.dumps``.
3987+
3988+
Args:
3989+
obj (Any): A GeoJSON dict, list, or scalar that may contain NumPy types.
3990+
3991+
Returns:
3992+
Any: The same structure with NumPy arrays converted to lists and NumPy
3993+
scalars to native Python types.
3994+
"""
3995+
if isinstance(obj, np.ndarray):
3996+
return obj.tolist()
3997+
elif isinstance(obj, np.generic):
3998+
return obj.item()
3999+
elif isinstance(obj, dict):
4000+
return {key: sanitize_geojson(value) for key, value in obj.items()}
4001+
elif isinstance(obj, (list, tuple)):
4002+
return [sanitize_geojson(value) for value in obj]
4003+
return obj
4004+
4005+
39584006
def geojson_to_df(in_geojson, encoding="utf-8", drop_geometry=True):
39594007
"""Converts a GeoJSON object to a pandas DataFrame.
39604008

leafmap/foliumap.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,10 +2046,10 @@ def add_geojson(
20462046
with open(output, "r") as fd:
20472047
data = json.load(fd)
20482048
else:
2049-
gdf = gpd.read_file(in_geojson, encoding=encoding)
2049+
gdf = geojson_to_gdf(in_geojson, encoding=encoding)
20502050

20512051
else:
2052-
gdf = gpd.read_file(in_geojson, encoding=encoding)
2052+
gdf = geojson_to_gdf(in_geojson, encoding=encoding)
20532053

20542054
elif isinstance(in_geojson, dict):
20552055
gdf = gpd.GeoDataFrame.from_features(in_geojson)
@@ -2067,7 +2067,7 @@ def add_geojson(
20672067
gdf.crs = "EPSG:4326"
20682068
elif gdf.crs != "EPSG:4326":
20692069
gdf = gdf.to_crs("EPSG:4326")
2070-
data = gdf.__geo_interface__
2070+
data = common.sanitize_geojson(gdf.__geo_interface__)
20712071

20722072
# interchangeable parameters between ipyleaflet and folium.
20732073

@@ -2411,11 +2411,16 @@ def add_vector(
24112411
opacity (float, optional): The opacity of the layer. Defaults to 1.0.
24122412
24132413
"""
2414-
import fiona
24152414
import geopandas as gpd
24162415

24172416
if isinstance(filename, str) and filename.endswith(".kml"):
2418-
fiona.drvsupport.supported_drivers["KML"] = "rw"
2417+
try:
2418+
import fiona
2419+
2420+
fiona.drvsupport.supported_drivers["KML"] = "rw"
2421+
except ImportError:
2422+
# fiona is optional; the default pyogrio engine reads KML natively.
2423+
pass
24192424
gdf = gpd.read_file(
24202425
filename,
24212426
bbox=bbox,

0 commit comments

Comments
 (0)