Refactor GeoTIFF Phase 5c-read: extract _layout.py from _reader.py#2257
Merged
Conversation
…2247) Move read-side layout / validation helpers out of ``_reader.py`` into a new ``_layout`` module so the top-level reader keeps just orchestration and the COG-HTTP transport layer. Moved: - ``MAX_PIXELS_DEFAULT``, ``PixelSafetyLimitError``, ``_check_dimensions``, ``_check_source_dimensions`` - ``_sparse_fill_value``, ``_has_sparse`` - ``_FULL_IMAGE_BUDGET_HEADER_SLACK``, ``_compute_full_image_byte_budget`` - ``_ifd_required_extent`` ``_reader.py`` re-exports each name so existing ``from xrspatial.geotiff._reader import ...`` call sites and test monkeypatches against ``_reader.MAX_PIXELS_DEFAULT`` keep working. ``_decode.py`` switches to importing the helpers from ``_layout`` directly and keeps the ``_reader.MAX_PIXELS_DEFAULT`` lookup so the existing monkeypatch contract on the per-tile safety check still fires. Mechanical, behaviour-neutral. ``_reader.py`` drops from 1406 to 1258 lines; the remaining bulk is the COG-HTTP transport which the issue scopes out of this PR.
brendancol
commented
May 21, 2026
Contributor
Author
brendancol
left a comment
There was a problem hiding this comment.
PR Review: Refactor GeoTIFF Phase 5c-read: extract _layout.py from _reader.py
Mechanical extraction. Code lifts cleanly, the re-export in _reader.py keeps every historical import working, and the documented _reader.MAX_PIXELS_DEFAULT monkeypatch contract is preserved.
Blockers
None.
Suggestions
None blocking.
Nits
xrspatial/geotiff/_layout.py:23--TIFFHeaderis imported and appears in_ifd_required_extent's signature but is unused in the body (pre-existing in_reader.py, just travels with the move). Same for thedata_len: intparameter on that function. Worth pruning in a follow-up cleanup PR, not here -- this PR is scoped to a behaviour-neutral move.xrspatial/geotiff/_layout.py:91and:154-- the lazyfrom ._decode import _int_nodata_in_rangeandfrom ._sources import _max_tile_bytes_from_envare defensible (they keep_layoutimport-cheap and side-step a load-order surprise if_decodeis re-shuffled later), but_decodedoes not import_layoutat top level and_sourceshas no path back to_layouteither, so both could be hoisted to module-level imports without creating a cycle. The lazy form is the safer choice; just flagging that it is a choice, not a constraint.xrspatial/geotiff/_decode.py:562-- the split (MAX_PIXELS_DEFAULTfrom_reader, the rest from_layout) is intentional and well-commented for monkeypatch compatibility. If a future cleanup unifies the patch surface (e.g. patch_layout.MAX_PIXELS_DEFAULTand re-export downstream), this hop can collapse.
What looks good
_reader.pykeeps the re-export block prominent at the top with a clear "Source: PR-H, issue #2247" pointer._decode.pydocstring is updated to reflect the new layout-module relationship and explains exactly why the lazy imports stay lazy.- The monkeypatch contract (
test_vrt_source_tile_check_1823::test_per_tile_check_caps_at_default) is the load-bearing test; it passes locally, which confirms the_reader -> _layoutaliasing did not break the rebinding semantics. - Documented circular-relationship management at the head of
_decode.pyis accurate and pays off when reading_layout.py's lazy imports.
Checklist
- Mechanical move only; no algorithm changes (correctness review is N/A)
- No backend dispatch changes
- No NaN-handling changes
- Test contract preserved (5037 passed locally; one pre-existing lz4 failure unrelated to this PR)
- No dask chunk-boundary changes
- No new materialization
- No benchmark needed (no perf-sensitive code touched)
- No README feature-matrix change needed
- Docstrings preserved verbatim on moved functions
- Drop unused TIFFHeader import and unused header/data_len params from _ifd_required_extent. The function never read them; they traveled with the move from _reader.py. - Hoist lazy imports (_int_nodata_in_range, _parse_nodata_str, _max_tile_bytes_from_env) to module-level. No cycle exists: _decode and _sources do not import _layout. Leaves the _decode.py MAX_PIXELS_DEFAULT patch-surface split alone (the third nit) since it is load-bearing for monkeypatch compatibility.
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
Phase 5c-read of the GeoTIFF refactor epic. Move read-side layout and validation helpers out of
_reader.pyinto a new_layout.py.Moved
MAX_PIXELS_DEFAULT,PixelSafetyLimitError_check_dimensions,_check_source_dimensions_sparse_fill_value,_has_sparse_FULL_IMAGE_BUDGET_HEADER_SLACK,_compute_full_image_byte_budget_ifd_required_extentKept in
_reader.py_read_to_array, theread_to_arrayalias._parse_cog_http_meta,_read_cog_http,_fetch_decode_cog_http_strips,_fetch_decode_cog_http_tiles. The issue scopes the COG-HTTP extraction to a separate follow-up.Compatibility
_reader.pyre-exports every moved name, sofrom xrspatial.geotiff._reader import PixelSafetyLimitError(and friends) keeps working for the backends, the VRT module, and the test suite._decode.pystill readsMAX_PIXELS_DEFAULTvia_reader, so the existingmonkeypatch.setattr(reader_mod, "MAX_PIXELS_DEFAULT", N)contract on the per-tile safety check keeps firing.Line count
_reader.py: 1406 to 1258 (148 lines moved)._layout.pyis 217 lines.Test plan
pytest xrspatial/geotiff/tests/locally: 5037 passed, 68 skipped. One pre-existinglz4failure (test_lowlevel_write_pushdown_2138.py::test_write_vs_to_geotiff_byte_parity_uint8[lz4]) reproduces onmainand is unrelated.Closes #2247
Part of #2211