Reject cog=True with tiled=False at the writer boundary (#2312)#2318
Merged
Conversation
The COG specification requires a tiled internal layout. The writer used to accept cog=True with tiled=False, warn that tile_size was ignored, then write strips through _write -- producing a malformed file that claimed to be a COG. Reject the combination at to_geotiff's public boundary and add a defense-in-depth gate in _writer._write so direct callers cannot bypass the wrapper. Comment the now-dead tile_size-ignored warning arm for the cog=True case. Tests in test_cog_requires_tiled_2312.py pin the rejection (public and low-level), the absence of the misleading tile_size warning on the cog=True arm, the valid tiled-COG smoke path, and the cog=False/tiled=False strip path as a negative control.
brendancol
commented
May 22, 2026
Contributor
Author
brendancol
left a comment
There was a problem hiding this comment.
PR Review: Reject cog=True with tiled=False at the writer boundary (#2312)
Blockers (must fix before merge)
None.
Suggestions (should fix, not blocking)
-
xrspatial/geotiff/_writers/eager.pylines 143-150 (tileddocstring entry): the new rejection is invisible to a reader scanning only that entry. Add a sentence sayingtiled=Falseis incompatible withcog=Trueand raisesValueError. Symmetric with thetile_sizeentry that already documents the strip-mode warning. -
xrspatial/geotiff/_writers/eager.pylines 159-163 (cogdocstring entry): mirror the same note on thecogside. A caller reading "how do I write a COG" lands here first, so thetiled=Truerequirement should appear here too.
Nits (optional improvements)
-
xrspatial/geotiff/_writer.pyline 481 andxrspatial/geotiff/_writers/eager.pyline 569: the twoValueErrormessage strings are byte-identical. If one gets reworded, the other will drift. Pulling the message into a module-level constant (e.g._COG_REQUIRES_TILED_MSG) would keep them in sync. The tests already pin substrings on both sides, so the drift risk is low; the constant is just an explicit invariant. -
xrspatial/geotiff/_writer.pyline 473: the defense-in-depth gate is inline rather than inside_validate_lowlevel_write_kwargs(line 135), where the other push-down byte-affecting checks live. Moving it would broaden that helper's signature withtiledandcog, which may not be worth it. Flagging only for consideration; the inline gate is fine.
What looks good
- The error message is actionable: it names the violated constraint (COG spec requires tiled layout), the two fixes the caller can apply (
tiled=Trueorcog=False), and links to the issue. Same shape as the COG gates pinned in PR #2301 / commit f5fbad5. - The defense-in-depth gate in
_writer._writeis at the right depth. It fires before_write_strippedruns and before the overview-pyramid block, so a direct caller cannot produce the malformed strip-plus-overviews file by bypassingto_geotiff. - Test file is well-structured: separate rows for the public rejection, the low-level rejection, the "tile_size warning must not fire" pin, both happy-path smoke tests (explicit and default
tiled), and a negative control for the strip-without-COG path. - The dead "tile_size is ignored" warning branch is documented in place rather than deleted. The branch still serves the
cog=False, tiled=False, tile_size != 256case, so the comment correctly scopes the change to thecog=Truearm. - Temp file names carry the
_2312issue suffix, consistent with the project convention.
Checklist
- Algorithm matches reference/paper: COG spec requires tiled layout.
- All implemented backends produce consistent results: the eager and array-level entry points both gate the combination; dask streaming falls through to
_write(which gates again); the GPU path already rejectedtiled=Falseindependently. - NaN handling: not applicable (validation-only change).
- Edge cases covered by tests: explicit
tiled=False, withtile_size, defaulttiled, strip-without-COG control. - Dask chunk boundaries handled correctly: not applicable (no dask compute paths changed).
- No premature materialization or unnecessary copies: not applicable.
- Benchmark exists or is not needed: not needed.
- README feature matrix updated: not applicable (no new function or backend change).
- Docstrings present: the test file docstring documents the new ValueError; the suggestion above is to also mention the constraint on the
tiled/cogparameter entries in the public docstring.
Apply review suggestions from PR #2318: - Document the cog=True / tiled=False incompatibility in the public to_geotiff docstring on both the tiled and cog parameter entries. A reader scanning either entry now sees the constraint without having to discover it by raising ValueError. - Extract the two byte-identical ValueError message strings into a module-level constant _COG_REQUIRES_TILED_MSG in _writer.py and import it into _writers/eager.py. Eliminates the drift risk between the public-boundary raise and the defense-in-depth raise. The substring assertions in test_cog_requires_tiled_2312.py still pin the actionable tokens (tiled=True, cog=False, COG) on both raise sites, so the contract is unchanged. The other review nit (move the gate inside _validate_lowlevel_write_kwargs) is dismissed: that helper would have to grow tiled and cog params, which broadens its signature past what the rest of its checks need. The inline gate is fine.
brendancol
commented
May 22, 2026
Contributor
Author
brendancol
left a comment
There was a problem hiding this comment.
PR Review (follow-up): Reject cog=True with tiled=False at the writer boundary (#2312)
Re-review after commit dfc617f.
Status of original findings
Suggestions (both fixed):
-
tileddocstring entry now saystiled=Falseis incompatible withcog=Trueand raisesValueError. -
cogdocstring entry now says the parameter requirestiled=True.
Nits:
- Fixed: shared error message extracted to
_COG_REQUIRES_TILED_MSGin_writer.py, imported into_writers/eager.py. Both raise sites use the constant, so a future reword cannot make them drift. - Dismissed: moving the defense-in-depth gate into
_validate_lowlevel_write_kwargswould require addingtiledandcogparameters to that helper. Its existing signature only carries kwargs whose validation is independent of the layout decision (compression, max_z_error, crs_*, allow_unparseable_crs). Two more parameters for one check is wider than the consolidation is worth. The inline gate is fine and the comment at the call site explains why it lives there.
New checks
No new findings. The 6 tests in test_cog_requires_tiled_2312.py still pass after the follow-up commit; the wider test_cog_invalid_input_errors_2286.py suite (19 tests) is unaffected.
What looks good (continued)
The shared constant lives in _writer.py (the array-level module), which is the lower of the two modules that raise the error. Both raise sites import from a single source of truth. The docstring updates are parameter-scoped, so the rest of the long to_geotiff docstring did not shift, and the diff stays narrow.
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.
Summary
Closes #2312.
cog=Truecombined withtiled=Falseat theto_geotiffpublic boundary with aValueErrorthat names the COG-spec constraint and both caller-side fixes (tiled=Trueorcog=False). Same shape as the COG input gates pinned in PR Pin actionable failure modes for unsupported COG writer inputs (#2286 prod-ready wave B) #2301 / commit f5fbad5._writer._writeso direct callers of the array-level entry point cannot bypass the public wrapper to produce the malformed file.cog=Truearm of the "tile_size is ignored when tiled=False" warning. Under the new gate,cog=Truealways impliestiled=True, so the warning fires only on thecog=False, tiled=Falsepath now.The writer used to silently produce a non-COG strip TIFF for
cog=True, tiled=False, violating the stable COG contract promoted in #2300.Backend coverage
Writer-side fix. Both the eager (
to_geotiff) and array-level (_write) entry points gate the combination, which covers the numpy and dask CPU paths. The GPU writer (write_geotiff_gpu) already rejectstiled=Falseindependently, so the four-backend matrix (numpy / cupy / dask+numpy / dask+cupy) is covered.Test plan
to_geotiff(cog=True, tiled=False)raises with the expected message tokens.tile_sizekwarg withcog=True, tiled=Falsestill raises and does not emit the misleading "tile_size is ignored" warning._writer.write(cog=True, tiled=False)also raises.cog=True, tiled=True) still round-trips.cog=Truewith defaulttiledstill works.cog=False, tiled=Falsestrip path still works.xrspatial/geotiff/tests/suite: 5220 passed, 68 skipped locally.