Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/docs-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]
python-version: ["3.13"]

env:
PLANET_API_KEY: ${{ secrets.PLANET_API_KEY }}
Expand Down Expand Up @@ -51,18 +51,18 @@ jobs:

- name: Test import
run: |
uv run python -c "import leafmap; print('leafmap import successful')"
# uv run python -c "from osgeo import gdal; print('gdal import successful')"
# uv run gdalinfo --version
.venv/bin/python -c "import leafmap; print('leafmap import successful')"
# .venv/bin/python -c "from osgeo import gdal; print('gdal import successful')"
# .venv/bin/gdalinfo --version

- name: Running pytest
run: |
uv run pytest . --verbose
.venv/bin/pytest . --verbose

- name: Install mkdocs
run: |
uv pip install -r requirements_docs.txt
uv run mkdocs build
.venv/bin/mkdocs build

- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v4.0
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12"]
python-version: ["3.13"]

env:
PLANET_API_KEY: ${{ secrets.PLANET_API_KEY }}
Expand Down Expand Up @@ -50,15 +50,15 @@ jobs:

- name: Test import
run: |
uv run python -c "import leafmap; print('leafmap import successful')"
# uv run python -c "from osgeo import gdal; print('gdal import successful')"
# uv run gdalinfo --version
.venv/bin/python -c "import leafmap; print('leafmap import successful')"
# .venv/bin/python -c "from osgeo import gdal; print('gdal import successful')"
# .venv/bin/gdalinfo --version

- name: Running pytest
run: |
uv run pytest . --verbose
.venv/bin/pytest . --verbose

- name: Install mkdocs
run: |
uv pip install -r requirements_docs.txt
uv run mkdocs gh-deploy --force
.venv/bin/mkdocs gh-deploy --force
6 changes: 3 additions & 3 deletions .github/workflows/installation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.12"]
python-version: ["3.13", "3.14"]
steps:
- uses: actions/checkout@v7

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.4.16"
version: "latest"
# enable-cache: true

- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -32,4 +32,4 @@ jobs:
uv pip install .

- name: Test import
run: uv run python -c "import leafmap; print('leafmap import successful')"
run: .venv/bin/python -c "import leafmap; print('leafmap import successful')"
7 changes: 4 additions & 3 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ jobs:
fail-fast: false
matrix:
config:
- { os: macOS-latest, py: "3.12" }
- { os: macOS-latest, py: "3.13" }
- { os: macOS-latest, py: "3.14" }
env:
SDKROOT: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
steps:
Expand All @@ -24,7 +25,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.4.16"
version: "latest"
# enable-cache: true

- name: Set up Python ${{ matrix.config.py }}
Expand All @@ -37,4 +38,4 @@ jobs:

- name: Test import
run: |
uv run python -c "import leafmap; print('leafmap import successful')"
.venv/bin/python -c "import leafmap; print('leafmap import successful')"
12 changes: 6 additions & 6 deletions .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.11", "3.12", "3.13", "3.14"]

env:
PLANET_API_KEY: ${{ secrets.PLANET_API_KEY }}
Expand All @@ -33,7 +33,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.4.16"
version: "latest"
# enable-cache: true

- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -52,10 +52,10 @@ jobs:

- name: Test import
run: |
uv run python -c "import leafmap; print('leafmap import successful')"
# uv run python -c "from osgeo import gdal; print('gdal import successful')"
# uv run gdalinfo --version
.venv/bin/python -c "import leafmap; print('leafmap import successful')"
# .venv/bin/python -c "from osgeo import gdal; print('gdal import successful')"
# .venv/bin/gdalinfo --version

- name: Running pytest
run: |
uv run pytest . --verbose
.venv/bin/pytest . --verbose
6 changes: 3 additions & 3 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.12"]
python-version: ["3.13", "3.14"]

env:
PLANET_API_KEY: ${{ secrets.PLANET_API_KEY }}
Expand All @@ -29,7 +29,7 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.4.16"
version: "latest"
# enable-cache: true

- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -42,4 +42,4 @@ jobs:

- name: Test import
run: |
uv run python -c "import leafmap; print('leafmap import successful')"
.venv\Scripts\python -c "import leafmap; print('leafmap import successful')"
66 changes: 57 additions & 9 deletions leafmap/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1516,11 +1516,15 @@ def kml_to_shp(in_kml, out_shp):

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

import fiona
import geopandas as gpd

# print(fiona.supported_drivers)
fiona.drvsupport.supported_drivers["KML"] = "rw"
try:
import fiona

fiona.drvsupport.supported_drivers["KML"] = "rw"
except ImportError:
# fiona is optional; the default pyogrio engine reads KML natively.
pass
df = gpd.read_file(in_kml, driver="KML")
df.to_file(out_shp)

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

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

import fiona
import geopandas as gpd

# import fiona
# print(fiona.supported_drivers)
fiona.drvsupport.supported_drivers["KML"] = "rw"
try:
import fiona

fiona.drvsupport.supported_drivers["KML"] = "rw"
except ImportError:
# fiona is optional; the default pyogrio engine reads KML natively.
pass
gdf = gpd.read_file(in_kml, driver="KML")

if out_geojson is not None:
Expand Down Expand Up @@ -1693,7 +1700,6 @@ def vector_to_geojson(

warnings.filterwarnings("ignore")
check_package(name="geopandas", URL="https://geopandas.org")
import fiona
import geopandas as gpd

if not filename.startswith("http"):
Expand All @@ -1702,7 +1708,13 @@ def vector_to_geojson(
filename = "zip://" + filename
ext = os.path.splitext(filename)[1].lower()
if ext == ".kml":
fiona.drvsupport.supported_drivers["KML"] = "rw"
try:
import fiona

fiona.drvsupport.supported_drivers["KML"] = "rw"
except ImportError:
# fiona is optional; the default pyogrio engine reads KML natively.
pass
df = gpd.read_file(
filename,
bbox=bbox,
Expand Down Expand Up @@ -3950,11 +3962,47 @@ def geojson_to_gdf(in_geojson, encoding="utf-8", **kwargs: Any):
with open(out_file, "w") as f:
json.dump(in_geojson, f)
in_geojson = out_file
elif isinstance(in_geojson, str) and in_geojson.startswith("http"):
try:
return gpd.read_file(in_geojson, encoding=encoding, **kwargs)
except Exception:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Blind except Exception swallows the original fetch/parse error.

except Exception: (flagged by ruff BLE001) silently discards the failure reason from the initial gpd.read_file attempt, making it hard to diagnose why the fallback path was triggered when the fallback itself later fails.

♻️ Suggested improvement
-        except Exception:
+        except Exception as e:
+            logger.debug(f"gpd.read_file failed for {in_geojson}, falling back to requests: {e}")
             response = requests.get(in_geojson, timeout=30)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception:
except Exception as e:
logger.debug(f"gpd.read_file failed for {in_geojson}, falling back to requests: {e}")
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 3956-3956: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.get(in_geojson, timeout=30)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(ssrf-requests)

🪛 Ruff (0.15.20)

[warning] 3956-3956: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@leafmap/common.py` at line 3956, The fallback handling around the initial
gpd.read_file attempt is swallowing the original fetch/parse failure with a
blind except Exception. Update this block to capture the exception as a variable
and preserve it when entering the fallback path so the original error is not
lost if the fallback also fails; keep the change localized to the
read_file/fallback logic in common.py and ensure the eventual failure message
includes the first error details.

Source: Linters/SAST tools

response = requests.get(in_geojson, timeout=30)
response.raise_for_status()
data = response.json()
if isinstance(data, dict) and data.get("type") == "Feature":
return gpd.GeoDataFrame.from_features([data], **kwargs)
return gpd.GeoDataFrame.from_features(data, **kwargs)
Comment on lines +3965 to +3974

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does geopandas GeoDataFrame.from_features accept arbitrary keyword arguments beyond crs and columns?

💡 Result:

No, geopandas.GeoDataFrame.from_features does not accept arbitrary keyword arguments [1][2]. The method signature is explicitly defined as classmethod GeoDataFrame.from_features(features, crs=None, columns=None), and it only supports the parameters 'features', 'crs', and 'columns' [1][2][3]. Passing additional keyword arguments will result in a TypeError.

Citations:


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
path = Path("leafmap/common.py")
lines = path.read_text().splitlines()
start, end = 3935, 3970
for i in range(start, min(end, len(lines)) + 1):
    print(f"{i:5d}: {lines[i-1]}")
PY

Repository: opengeos/leafmap

Length of output: 1778


Drop unsupported kwargs in the HTTP fallback. gpd.GeoDataFrame.from_features() only accepts crs and columns, so forwarding the full **kwargs from gpd.read_file() here can turn the fallback into a new TypeError and mask the original error. Pass only the supported kwargs in this branch.

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 3956-3956: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.get(in_geojson, timeout=30)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(ssrf-requests)

🪛 Ruff (0.15.20)

[warning] 3956-3956: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@leafmap/common.py` around lines 3953 - 3962, The HTTP fallback in the GeoJSON
loading branch is forwarding all of `**kwargs` into
`gpd.GeoDataFrame.from_features()`, but that method only supports a limited
subset and can raise a new `TypeError`. Update the `in_geojson` HTTP fallback
inside `common.py` to pass only supported arguments (for example, `crs` and
`columns`) to `GeoDataFrame.from_features()`, and keep the original
`gpd.read_file()` call unchanged.


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


def sanitize_geojson(obj: Any) -> Any:
"""Recursively converts NumPy types in a GeoJSON-like object to native Python types.

GeoDataFrame.__geo_interface__ can leave NumPy arrays/scalars in feature
properties (e.g. list-valued columns are read back as ndarrays), which are
not JSON serializable and break widget serialization. This makes the object
safe for ``json.dumps``.

Args:
obj (Any): A GeoJSON dict, list, or scalar that may contain NumPy types.

Returns:
Any: The same structure with NumPy arrays converted to lists and NumPy
scalars to native Python types.
"""
if isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, np.generic):
return obj.item()
elif isinstance(obj, dict):
return {key: sanitize_geojson(value) for key, value in obj.items()}
elif isinstance(obj, (list, tuple)):
return [sanitize_geojson(value) for value in obj]
return obj


def geojson_to_df(in_geojson, encoding="utf-8", drop_geometry=True):
"""Converts a GeoJSON object to a pandas DataFrame.

Expand Down
15 changes: 10 additions & 5 deletions leafmap/foliumap.py
Original file line number Diff line number Diff line change
Expand Up @@ -2046,10 +2046,10 @@ def add_geojson(
with open(output, "r") as fd:
data = json.load(fd)
else:
gdf = gpd.read_file(in_geojson, encoding=encoding)
gdf = geojson_to_gdf(in_geojson, encoding=encoding)

else:
gdf = gpd.read_file(in_geojson, encoding=encoding)
gdf = geojson_to_gdf(in_geojson, encoding=encoding)

elif isinstance(in_geojson, dict):
gdf = gpd.GeoDataFrame.from_features(in_geojson)
Expand All @@ -2067,7 +2067,7 @@ def add_geojson(
gdf.crs = "EPSG:4326"
elif gdf.crs != "EPSG:4326":
gdf = gdf.to_crs("EPSG:4326")
data = gdf.__geo_interface__
data = common.sanitize_geojson(gdf.__geo_interface__)

# interchangeable parameters between ipyleaflet and folium.

Expand Down Expand Up @@ -2411,11 +2411,16 @@ def add_vector(
opacity (float, optional): The opacity of the layer. Defaults to 1.0.

"""
import fiona
import geopandas as gpd

if isinstance(filename, str) and filename.endswith(".kml"):
fiona.drvsupport.supported_drivers["KML"] = "rw"
try:
import fiona

fiona.drvsupport.supported_drivers["KML"] = "rw"
except ImportError:
# fiona is optional; the default pyogrio engine reads KML natively.
pass
gdf = gpd.read_file(
filename,
bbox=bbox,
Expand Down
Loading
Loading