Make shapely an optional [vector] extra (#2496)#2497
Merged
Conversation
shapely (and GEOS) loaded on every import xrspatial because rasterize.py imported it at module top level. Import it lazily via a cached _require_shapely() helper, bound locally in each function that uses the shapely array API (so dask tile workers, which call the helpers directly, also get the friendly error). rasterize() calls it up front for an early, clear failure. Remove shapely>=2.0 from install_requires; add a vector extra and keep shapely in the tests extra. polygonize already imports shapely lazily and only on its geopandas return path, so it needs no change. Update README, install docs, and CHANGELOG.
brendancol
commented
May 27, 2026
brendancol
left a comment
Contributor
Author
There was a problem hiding this comment.
PR Review: Make shapely an optional [vector] extra (#2496)
Packaging change plus lazy-import plumbing. The rasterize algorithm and its
four backends are untouched, so the numeric/backend checklist mostly doesn't
apply here. I focused on whether every shapely use path is actually guarded.
Blockers
None.
Suggestions
None blocking.
Nits
polygonize.pystill does a bareimport shapelyinside_to_geopandas
(around line 609). Without shapely that raises a plain ImportError rather than
the[vector]-pointing message. In practice geopandas is imported first on
that same path and geopandas requires shapely, so the user hits geopandas's
own error first -- but the messaging is slightly inconsistent with the new
rasterize behavior. Optional to route it through_require_shapelyfor a
uniform message.test_optional_shapely.py::test_import_xrspatial_without_shapelyimports
xrspatial.rasterizein the subprocess but notxrspatial.polygonize. Adding
polygonize to that import list would lock in that it also imports clean
without shapely.
What looks good
- Every bare
shapely.reference (13 of them) sits in a function that binds
shapely = _require_shapely()first;rasterize()calls the guard up front
for an early, clear failure. Verified each call site against its enclosing
function. - The dask path is handled correctly: tile workers call
_polys_from_wkb/
_classify_geometriesdirectly, not throughrasterize(), and those helpers
bind via_require_shapely()so the worker either imports shapely or raises
the friendly error. The cache lives in a module global, so each worker
process resolves it once. - Confirmed
import xrspatialno longer loads shapely (absent from
sys.modules), and a real rasterize still produces correct output. - shapely kept in the
testsextra, so CI still exercises the vector paths;
356 rasterize/polygonize tests pass. - New tests cover the subprocess import, the helper message, and the up-front
rasterize()error, and they reset the_shapelycache so the in-process
checks are real.
Checklist
- Every shapely use path guarded (rasterize + dask helpers)
-
import xrspatialno longer loads shapely - Friendly ImportError points at the
vectorextra - tests extra keeps shapely so CI exercises vector paths
- Docs (README, installation.rst) and CHANGELOG updated
- polygonize geopandas path uses the same
[vector]message (nit) - No backend/algorithm change; existing rasterize tests pass
brendancol
commented
May 27, 2026
brendancol
left a comment
Contributor
Author
There was a problem hiding this comment.
Follow-up review (after 7047f03)
Disposition of the two nits from the first pass:
- Nit 2 (subprocess import test): fixed.
test_import_xrspatial_without_shapely
now also importsxrspatial.polygonizewith shapely blocked, locking in that
it imports clean too. 3 passed. - Nit 1 (route polygonize's geopandas path through
_require_shapely):
dismissed. On that pathimport geopandasruns first, and geopandas
hard-requires shapely, so a "geopandas installed but shapely missing" state is
not reachable. Adding a cross-module import for a message that can't surface
isn't worth it.
No remaining items beyond CI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2496
shapely (and GEOS) loaded on every
import xrspatialbecauserasterize.pyimported it at module top level, even though only
rasterizeandpolygonizeuse it. This moves it to an optional
vectorextra, matching the matplotlibchange in #2494.
import shapelyinrasterize.pywith a cached_require_shapely()helper. Each function that uses the shapely array APIbinds the module locally, so dask tile workers (which call the tile helpers
directly, not through
rasterize) also get the clear error.rasterize()calls it up front so a missing install fails early with a message pointing at
pip install xarray-spatial[vector].shapely>=2.0frominstall_requires; add avectorextra and keepshapely in the
testsextra so CI still exercises the vector paths.polygonizealready imports shapely lazily, and only on itsgeopandasreturn path (which needs geopandas anyway), so it needs no change.
This is a packaging change, not backend-specific. The rasterize backends
(numpy / cupy / dask+numpy / dask+cupy) are unchanged; shapely is only used for
geometry classification and coordinate extraction, which feed all backends.
Test plan
test_optional_shapely.py(3 passed):import xrspatialworks in asubprocess with shapely blocked;
_require_shapely()andrasterize()raisethe friendly error pointing at the
vectorextra.pytest xrspatial/tests/test_rasterize.py xrspatial/tests/test_polygonize.py(356 passed, 15 skipped) with shapely installed.
import xrspatialno longer loads shapely (shapelyabsentfrom
sys.modules) and a real rasterize still produces correct output.