Skip to content

refactor(cog)!: reduce to_cog's 29-parameter signature via grouped option dataclasses (S107) - #986

Open
MAfarrag wants to merge 14 commits into
mainfrom
refactor/to-cog-options-grouping
Open

refactor(cog)!: reduce to_cog's 29-parameter signature via grouped option dataclasses (S107)#986
MAfarrag wants to merge 14 commits into
mainfrom
refactor/to-cog-options-grouping

Conversation

@MAfarrag

@MAfarrag MAfarrag commented Aug 14, 2026

Copy link
Copy Markdown
Member

Description

Dataset.to_cog() accepted 29 keyword parameters, tripping SonarQube python:S107 (max 13). This groups the
low-level knobs into six typed, validated frozen dataclasses exposed under the cog namespace, reducing the
signature to 9 parameters and clearing S107 as a real decomposition (Option B from the issue), not a linter
work-around.

New groups (from pyramids.dataset import cog):

  • cog.Compressioncompress, level, quality, predictor, max_z_error
  • cog.Overviewsresampling, count, compress
  • cog.Tilingtarget_srs, resampling (warp), scheme, zoom_level, zoom_level_strategy, aligned_levels
  • cog.BandSelectionindexes, out_dtype, nodata
  • cog.Tagsband_tags, colormap, metadata
  • cog.Layoutblocksize, bigtiff, num_threads, add_mask, sparse_ok, statistics

New signature:

to_cog(path, *, compression=None, overviews=None, tiling=None,
       bands=None, tags=None, layout=None, config=None, extra=None)

Highlights:

  • compression accepts a profile-name string or a Compression (coerced via Compression.coerce), collapsing
    the old profile/compress/level/quality/predictor knobs into one concept. The common call stays terse:
    ds.to_cog("out.tif", compression="zstd").
  • Per-group validation now lives in each dataclass's __post_init__ (blocksize power-of-2, quality range, predictor
    value, zoom strategy, band indices), so invalid options fail at construction with a clear message.
  • jpeg/webp dtype/band constraints now also apply when the compress method is given directly, not only via a named
    profile (a strictly clearer, earlier error).
  • House write policy is unchanged: dtype-aware predictor, category-safe default overview resampling, and the
    STATISTICS retry — identical COG output for equivalent inputs.
  • Names are scoped to the cog namespace (not hoisted to top-level pyramids), so generic names like Compression
    stay free for other APIs; BandSelection avoids the existing engines.Bands collision.

Internal callers migrated: the pyramids cog create CLI command, grib_to_cog, and to_file(driver="COG").
write_cog was already extra=-based and is unaffected.

Issues

Type of change

Check relevant points.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Targeted COG suites re-run locally against the dev environment:

pixi run -e dev pytest tests/dataset/cog tests/dataset/collection/test_to_cog_stack.py \
  tests/dataset/io/test_to_bytes.py tests/dataset/io/test_grib.py \
  tests/netcdf/samples/test_inherited_ops.py tests/dataset/unit/test_engines.py
  • 584 passed, 0 failed across the COG write/validate/inspect suites, to_cog_stack, GRIB, and the
    NetCDF-inherited-ops sample.
  • Doctests on the changed modules pass (--doctest-modules src/pyramids/dataset/cog/options.py src/pyramids/dataset/engines/cog.py src/pyramids/dataset/cog/facade.py src/pyramids/cli.py → 11 passed, 12 skipped).
  • End-to-end smoke: grouped objects + compression="zstd" string coercion + dtype cast + all three validators
    firing; package imports with no cycles.

Checklist:

  • updated version number in pyproject.toml.
  • added changes to History.rst.
  • updated the latest version in README file.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • documentation are updated.

…asses

Replace to_cog's 29 flat keyword arguments with six grouped, validated frozen
dataclasses exposed under the `cog` namespace — Compression, Overviews, Tiling,
BandSelection, Tags, and Layout — reducing the signature to nine parameters and
clearing SonarQube python:S107.

- `compression` accepts a profile-name string or a Compression (coerced),
  collapsing the old profile/compress/level/quality/predictor knobs.
- Per-group validation now lives in each dataclass's __post_init__ (blocksize
  power-of-2, quality range, predictor value, zoom strategy, band indices), so
  invalid options fail at construction with a clear message.
- jpeg/webp dtype/band constraints now also apply when the compress method is
  given directly, not only via a named profile.
- House write policy is unchanged: dtype-aware predictor, category-safe default
  overview resampling, and the STATISTICS retry.
- Internal callers migrated: the CLI `cog create` command, grib_to_cog, and
  to_file(driver="COG"). ~47 test call sites migrated to the grouped API.

BREAKING CHANGE: Dataset.to_cog(), to_cog_bytes(), and
DatasetCollection.to_cog_stack() no longer accept the flat COG option keyword
arguments (compress, blocksize, predictor, overview_resampling, tiling_scheme,
indexes, colormap, ...). Pass the grouped `cog.*` dataclasses instead — e.g.
`ds.to_cog("out.tif", compression="zstd", layout=cog.Layout(blocksize=256))`.
A direct Compression(compress="JPEG"/"WEBP") now passes straight to GDAL as
the pre-refactor flat compress= did (GDAL accepts e.g. 4-band Byte JPEG); the
dtype/band constraint fires only when the method was chosen via a profile
string, restoring the prior behaviour and avoiding an input narrowing.
The COG driver's PREDICTOR option accepts YES/NO/STANDARD/FLOATING_POINT;
'NO' (disable the predictor) was rejected by the validator. Add it to the
allowed set and document the full token list.
BandSelection/Tags carry list/dict fields, so instances are not hashable and
their contents stay mutable — document that. Also drop the now-redundant quoted
forward-ref annotations on Compression.coerce (from __future__ annotations).
grib.py and a test docstring still named the removed flat kwargs
(to_cog(indexes=/target_srs=/profile=), compress="ZSTD"); point them at the
grouped forms (bands=BandSelection, tiling=Tiling, compression=).
…e/blocksize paths

Rename test_explicit_compress_overrides_profile (the to_cog profile+override
interaction is gone) to an honest Compression(compress=...) assertion, and add
CLI tests for 'cog create --profile deflate --compress ZSTD' (the replace()
override branch) and '--blocksize 256'.
Direct unit tests for every __post_init__ branch (Compression quality/predictor,
Overviews count, Tiling zoom strategy, BandSelection indices, Layout
blocksize/bigtiff) and Compression.coerce (profile expansion, lerc max_z_error,
None/identity/unknown-profile), closing the validator coverage gap.
_cmd_create coerced --profile to a Compression object, which set
from_profile=False in to_cog and skipped the jpeg/webp dtype/band pre-check —
a regression vs the Python compression="jpeg" path. Forward the profile string
when no --compress override is given (build an object only for the override),
and cover --profile jpeg on a float source with a test.
- L1: Compression accepts '1'/'2'/'3' (string) predictors the old flat API
  forwarded to GDAL; add a test.
- L2: guard test that every PROFILES entry uses only the keys
  Compression.coerce carries (COMPRESS/LEVEL/QUALITY/MAX_Z_ERROR).
- N1: clarify that the jpeg/webp dtype/band check applies to the profile-string
  form only, not a direct Compression object.
- N2: make the hashability note precise (unhashable once a list/dict field is
  populated).
The overview mermaid diagram showed the removed compress="ZSTD" kwarg; use
compression="zstd".
Both docs/examples/cog notebooks (run by the nbval notebooks CI) still called
removed flat kwargs (profile=/compress=/tiling_scheme=/indexes=/band_tags=/...),
which now raise TypeError. Migrate every cell to the cog.* grouped dataclasses;
re-executed both headless clean (nbconvert, exit 0). Notebooks kept
output-stripped.
docs/tutorials/cog.md documented the removed flat to_cog kwargs throughout;
rewrite the snippets against compression=C:/Program Files/Git/overviews=/tiling=/bands=/tags=/layout=.
…S3776)

The grouped-options rewrite pushed to_cog's cognitive complexity to 20 (>15).
Move the dtype-aware predictor/overview resolution, the categorical guardrail,
and the GDAL option-dict assembly into a _build_cog_defaults helper; behavior is
unchanged (identical creation-option dict).
The grouped-options migration nested a dataclass constructor inside several
pytest.raises/pytest.warns blocks, creating a second invocation (SonarCloud
S5778/S9088). Hoist the construction out (or test the constructor directly where
it is the thrower, e.g. Layout blocksize), and split one composite Tags
assertion (S9073).
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(cog): group to_cog's 29 parameters into typed cog.* option dataclasses refactor(cog): reduce to_cog's 29-parameter signature (python:S107)

1 participant