developmentseed / developmentseed/titiler-covjson
Extract a catching helper for per-item error handling over raising reads
- Dominant language
- Python
- Stars
- 1
- Forks
- 1
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 12
Description
`_read_multipoint` (in `factory.py`) samples each position with an explicit `try/except` inside a loop, because rio-tiler's `Reader.point` raises `PointOutsideBounds` rather than returning a value:
```python
samples: list[PointData | None] = []
for x, y in geometry.positions:
try:
samples.append(src_dst.point(x, y, coord_crs=read_crs, **band_kwargs, **dataset_kwargs))
except PointOutsideBounds:
samples.append(None)
```
This is the only place in the codebase today that iterates over a raising dependency and catches per item (verified by an AST sweep: every other `try/except` is a single, non-iterating one). But it will recur: `/trajectory` (#57, which ADR-0005 says reuses this slice's machinery) and a PointSeries endpoint (#18) both sample per-vertex/per-time and will replicate this loop. When the second such site appears, a small helper earns its keep.
**Proposal (light, not a monad).** A single helper that turns a raising call into a value-returning one:
```python
def catching(exc, fn, /, *args, **kwargs): # exc: type[E] | tuple[type[E], ...] -> T | E
try:
return fn(*args, **kwargs)
except exc as e:
return e
```
The loop becomes a comprehension:
```python
samples = [
catching(PointOutsideBounds, src_dst.point, x, y, coord_crs=read_crs, **band_kwargs, **dataset_kwargs)
for x, y in geometry.positions
]
```
**Design decisions (settled in discussion):**
- **Return the exception (`T | E`), not `None`.** `None` collapses "why did it fail". Returning the caught exception preserves the reason, which is what a caller needs when more than one failure can occur and they must be handled differently. It also composes with #73: today only `PointOutsideBounds` is caught (a genuine reader error like the WarpedVRT `ValueError` still propagates); the day that error should also become a per-position value, widen the caught set and the returned union lets the caller tell the two apart.
- **Catch specific types only, never bare `Exception`.** The caller names the exception(s) to trap; anything else propagates. This is what keeps it forward-safe rather than a bug-swallower.
- **Explicitly NOT a `Result`/`Either` monad with map/filter/functor operations.** Threading a `Result`/`Either` type through layers fights Python and its libraries, which expect exceptions; a reader also has to learn the custom type and its operators. `T | E` (or a thin wrapper) confined to one hop, discriminated with `isinstance`, stays Pythonic.
**Notes for whoever implements it:**
- The `T | E` union has a mild footgun: a caller who forgets to discriminate flows an exception object downstream as data. mypy flags attribute access on the union (as it does for `None`), but it is softer than a `None` that fails fast. A thin frozen `Ok`/`Err` wrapper is safer (you must unwrap) at the cost of being heavier. Decide "how light is light" then; do not pre-build the wrapper.
- `filter`-to-successes drops position alignment, which the multipoint path must NOT do: an out-of-bounds position is kept in place as a masked column, aligned to `geometry.positions`. The map/filter ergonomics are for future callers that genuinely reduce over results; this one wants "keep all, in order". Say so, so nobody "simplifies" `_read_multipoint` into a lossy filter.
- The current caller does not consume the detail: `pointdata_to_multipoint_input` masks an absent position regardless of why, so `None` and `PointOutsideBounds` are interchangeable for #65 alone. This is why the helper is deferred, not built now: it earns its keep at the multi-failure-mode point (#73) and the second sampling loop (#57 / #18), not at #65.
**Trigger to act:** when #57 or #18 adds the second raising-in-a-loop sampling site. Extracting for one caller now would be premature; the pattern and its design are captured here so the future implementer does not have to rediscover them (or reopen the monad question).
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in factory.py at _read_multipoint and review ADR-0005, then watch #57 or #18 for the second raising-in-a-loop sampling site. When triggered, extract the specific-exception catching helper, preserve exception details and position alignment, and verify that unrelated errors still propagate.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100