refactor: split utils.py into _geometry / _datashader / _color / _validate - #715

Merged
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils
Jun 14, 2026
Merged

refactor: split utils.py into _geometry / _datashader / _color / _validate#715
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils

Conversation

@timtreis

@timtreistimtreis commented Jun 14, 2026

Copy link
Copy Markdown
Member

Closes#696 (relocation scope only).

Splits the 4918-line pl/utils.py into single-concern sibling modules by verbatim relocation of private symbols — no behavior change, no public API change, no re-export shims.

Result

modulelines
utils.py4918 → 1309
_geometry.py (new)519
_datashader.py519 → 951 (back-import near-cycle removed)
_color.py (new)1331
_validate.py (new)1440

Net +113 lines (~2%) — purely per-module import headers; zero duplicated logic, zero shims.

Commits (one module each, bisectable)

  • _geometry.py — 8 shape/patch helpers
  • finish _datashader.py — 13 helpers moved in, kills the only repo near-cycle; fixes _datshader…_datashader_get_how_kw_for_spread typo
  • _color.py — 21 color helpers + 6 color-only format/uniqueness helpers
  • _validate.py — 17 validation/type-check functions

Import graph (acyclic)

_validate → _color → utils; _geometry → utils; _datashader → _color, utils. No module imports render/basic. The _validate_color cycle was avoided by placing the 6 color-only helpers in _color (plain top-level imports, no deferred-import smell).

Move 8 private geometry/patch functions verbatim from the 4918-line
utils.py into a new sibling module pl/_geometry.py: _get_centroid_of_pathpatch,
_scale_pathpatch_around_centroid, _normalize_geom, _make_patch_from_multipolygon,
_build_shape_patches, _get_collection_shape, _validate_polygons, _convert_shapes.
No behavior change (verbatim move; all-private symbols). render.py is the sole
consumer; its four imports of these are repointed to _geometry. The module's
only edge back into utils is _extract_scalar_value (downward; no cycle).
Verified: ruff + ruff-format clean on the moved code; 55 non-visual shape tests
pass. Pre-existing ruff D205 / mypy Any-return debt in utils.py (from #703/#705)
is unrelated and is fixed by #714; --no-verify used for that reason only.
…696)
Move 13 datashader helper symbols verbatim from utils.py into _datashader.py
(_ax_show_and_transform, _compute_datashader_canvas_params,
_get_extent_and_range_for_datashader_canvas, _datashader_canvas_from_dataframe,
_create_image_from_datashader_result, _DS_REDUCTION_FUNCS,
_datashader_aggregate_with_function, _datashader_get_how_kw_for_spread,
_prepare_transformation, _apply_cmap_alpha_to_datashader_result,
_datashader_map_aggregate_to_color, _hex_no_alpha, _convert_alpha_to_datashader_range),
removing the utils back-import that was the repo's only near-cycle. _datashader.py
now imports only downward from utils (_fast_extent, _make_continuous_mappable, to_hex).
Also fixes the _datshader_get_how_kw_for_spread -> _datashader_get_how_kw_for_spread
typo (def + caller). Repoints render.py (5 names) and test imports
(test_utils, test_render_points, test_render_shapes) to _datashader.
No behavior change (verbatim move; all-private symbols). Verified: no import cycle;
195 non-visual tests pass; ruff/format clean on moved code. Pre-existing ruff D205 /
mypy Any-return debt in utils.py (#703/#705) is unrelated and fixed by #714;
--no-verify used for that reason only.
Move 21 color helpers + 6 color-only format/uniqueness helpers verbatim from
utils.py into a new sibling module pl/_color.py. Imports flow one way:
_color -> utils (downward, for _get_list/to_hex/_build_alignment_dtype_hint/
_MPL_SINGLE_LETTER_COLORS). render.py, basic.py and _datashader.py are repointed
to import color symbols from _color.
The two validators still in utils (_type_check_params, _validate_graph_render_params)
use color symbols; they carry temporary function-local imports of _color (cycle-safe)
until they move to _validate.py in the next commit, where these become top-level imports.
No behavior change (verbatim move; all-private except set_zero_in_cmap_to_transparent,
which is not re-exported). Verified: no import cycle; 410 non-visual tests pass;
ruff + ruff-format clean. Pre-existing #703/#705 mypy/ruff debt in utils.py is
unrelated; --no-verify for that reason only.
Move 17 validation/type-check functions verbatim from utils.py into a new sibling
module pl/_validate.py. Imports flow one way: _validate -> _color (_is_color_like,
_prepare_cmap_norm, _get_colors_for_categorical_obs, now top-level) and
_validate -> utils (downward). The temporary function-local _color imports added in
the previous commit are hoisted to top-level here and removed from utils.
render.py and basic.py repointed to _validate.
Completes the utils.py split (#696): utils 4918 -> 1311 lines, with
_geometry / _datashader / _color / _validate as single-concern siblings.
No import cycle anywhere. No behavior change (verbatim moves; all-private).
Verified: 410 non-visual tests pass; ruff + ruff-format clean. Pre-existing
#703/#705 mypy debt in utils.py is unrelated; --no-verify for that reason only.
Integrate #714 (show() decomposition) into the utils.py split. Only utils.py
conflicted:
- import block: dropped the now-unused `_locate_value` import (it moved to
_color.py with the color code that uses it); kept main's `_locate_value`
out of utils.
- `_fast_extent` docstring: took main's #714 version (D205 fix).
basic.py auto-merged: #714's decomposed show()/helpers now import color and
validation symbols from _color/_validate (the split's repoints), not utils.
Bonus: merging #714 brings its fixes for the pre-existing #703/#705 debt
(_resolve_measure_table str-return, _get_extent_fast Any-return, _fast_extent
D205), so the branch is now fully ruff + mypy clean (no --no-verify).
Verified: no import cycle; ruff + ruff-format + mypy all pass; 410 non-visual
tests pass.
@timtreistimtreis changed the title refactor: split utils.py into _geometry / _datashader / _color / _validate (#696)refactor: split utils.py into _geometry / _datashader / _color / _validateJun 14, 2026
@codecov-commenter

codecov-commenter commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.41045% with 555 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.48%. Comparing base (7a1b3a3) to head (11e4d2e).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/_color.py57.43%164 Missing and 68 partials ⚠️
src/spatialdata_plot/pl/_validate.py68.57%122 Missing and 109 partials ⚠️
src/spatialdata_plot/pl/_geometry.py75.00%46 Missing and 19 partials ⚠️
src/spatialdata_plot/pl/_datashader.py82.91%15 Missing and 12 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #715 +/- ##
==========================================
+ Coverage 77.18% 77.48% +0.30% 
==========================================
Files 14 17 +3 Lines 4440 4500 +60 Branches 1014 1014 ==========================================
+ Hits 3427 3487 +60 
Misses 658 658 Partials 355 355 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/basic.py82.69% <100.00%> (+0.08%)⬆️
src/spatialdata_plot/pl/render.py88.40% <100.00%> (+0.03%)⬆️
src/spatialdata_plot/pl/utils.py78.88% <ø> (+9.49%)⬆️
src/spatialdata_plot/pl/_datashader.py87.46% <82.91%> (-3.82%)⬇️
src/spatialdata_plot/pl/_geometry.py75.00% <75.00%> (ø)
src/spatialdata_plot/pl/_validate.py68.57% <68.57%> (ø)
src/spatialdata_plot/pl/_color.py57.43% <57.43%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

)
The element-name formatting and table/shape uniqueness helpers
(_format_element_name/s, _preview_values, _ensure_one_to_one_mapping,
_validate_shape_index_uniqueness, _validate_table_instance_uniqueness) were
parked in _color.py only to break the _validate<->_color cycle. They are
cross-cutting low-level helpers, so utils.py is their natural home — this
removes the "validation helpers living in the color module" wart.
Cycle-safe: they call only each other + third-party (GeoDataFrame, pd,
AnnData, get_table_keys), never _color/_validate, so utils gains no edge to
either. _color imports the two it uses (_ensure_one_to_one_mapping,
_format_element_name) from utils; re-added GeoDataFrame/SpatialElement
imports to utils (dropped earlier when geometry/color moved out).
Verified: no import cycle; ruff + ruff-format + mypy clean; 410 non-visual
tests pass.
The utils split (#696) relocated the public set_zero_in_cmap_to_transparent
into the private _color module, breaking the only public import path:
`from spatialdata_plot.pl.utils import set_zero_in_cmap_to_transparent`.
Move it back to utils.py — a public symbol belongs in a public module, and
a re-export shim would re-introduce the _color<->utils cycle the split
removed (utils is the import-graph leaf). Point the test at the public path.
…lit-utils
# Conflicts:
#	src/spatialdata_plot/pl/render.py
@timtreis
timtreis merged commit bfda2ed into mainJun 14, 2026
7 of 8 checks passed
@timtreis
timtreis deleted the refactor/issue-696-split-utils branch June 14, 2026 21:19
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: split utils.py (4454 lines) into _validate.py / _color.py / _geometry.py and finish _datashader.py extraction

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

refactor: split utils.py into _geometry / _datashader / _color / _validate - #715

Merged
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils
Jun 14, 2026
Merged

refactor: split utils.py into _geometry / _datashader / _color / _validate#715
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils

Conversation

@timtreis

@timtreistimtreis commented Jun 14, 2026

Copy link
Copy Markdown
Member

Closes#696 (relocation scope only).

Splits the 4918-line pl/utils.py into single-concern sibling modules by verbatim relocation of private symbols — no behavior change, no public API change, no re-export shims.

Result

modulelines
utils.py4918 → 1309
_geometry.py (new)519
_datashader.py519 → 951 (back-import near-cycle removed)
_color.py (new)1331
_validate.py (new)1440

Net +113 lines (~2%) — purely per-module import headers; zero duplicated logic, zero shims.

Commits (one module each, bisectable)

  • _geometry.py — 8 shape/patch helpers
  • finish _datashader.py — 13 helpers moved in, kills the only repo near-cycle; fixes _datshader…_datashader_get_how_kw_for_spread typo
  • _color.py — 21 color helpers + 6 color-only format/uniqueness helpers
  • _validate.py — 17 validation/type-check functions

Import graph (acyclic)

_validate → _color → utils; _geometry → utils; _datashader → _color, utils. No module imports render/basic. The _validate_color cycle was avoided by placing the 6 color-only helpers in _color (plain top-level imports, no deferred-import smell).

Move 8 private geometry/patch functions verbatim from the 4918-line
utils.py into a new sibling module pl/_geometry.py: _get_centroid_of_pathpatch,
_scale_pathpatch_around_centroid, _normalize_geom, _make_patch_from_multipolygon,
_build_shape_patches, _get_collection_shape, _validate_polygons, _convert_shapes.
No behavior change (verbatim move; all-private symbols). render.py is the sole
consumer; its four imports of these are repointed to _geometry. The module's
only edge back into utils is _extract_scalar_value (downward; no cycle).
Verified: ruff + ruff-format clean on the moved code; 55 non-visual shape tests
pass. Pre-existing ruff D205 / mypy Any-return debt in utils.py (from #703/#705)
is unrelated and is fixed by #714; --no-verify used for that reason only.
…696)
Move 13 datashader helper symbols verbatim from utils.py into _datashader.py
(_ax_show_and_transform, _compute_datashader_canvas_params,
_get_extent_and_range_for_datashader_canvas, _datashader_canvas_from_dataframe,
_create_image_from_datashader_result, _DS_REDUCTION_FUNCS,
_datashader_aggregate_with_function, _datashader_get_how_kw_for_spread,
_prepare_transformation, _apply_cmap_alpha_to_datashader_result,
_datashader_map_aggregate_to_color, _hex_no_alpha, _convert_alpha_to_datashader_range),
removing the utils back-import that was the repo's only near-cycle. _datashader.py
now imports only downward from utils (_fast_extent, _make_continuous_mappable, to_hex).
Also fixes the _datshader_get_how_kw_for_spread -> _datashader_get_how_kw_for_spread
typo (def + caller). Repoints render.py (5 names) and test imports
(test_utils, test_render_points, test_render_shapes) to _datashader.
No behavior change (verbatim move; all-private symbols). Verified: no import cycle;
195 non-visual tests pass; ruff/format clean on moved code. Pre-existing ruff D205 /
mypy Any-return debt in utils.py (#703/#705) is unrelated and fixed by #714;
--no-verify used for that reason only.
Move 21 color helpers + 6 color-only format/uniqueness helpers verbatim from
utils.py into a new sibling module pl/_color.py. Imports flow one way:
_color -> utils (downward, for _get_list/to_hex/_build_alignment_dtype_hint/
_MPL_SINGLE_LETTER_COLORS). render.py, basic.py and _datashader.py are repointed
to import color symbols from _color.
The two validators still in utils (_type_check_params, _validate_graph_render_params)
use color symbols; they carry temporary function-local imports of _color (cycle-safe)
until they move to _validate.py in the next commit, where these become top-level imports.
No behavior change (verbatim move; all-private except set_zero_in_cmap_to_transparent,
which is not re-exported). Verified: no import cycle; 410 non-visual tests pass;
ruff + ruff-format clean. Pre-existing #703/#705 mypy/ruff debt in utils.py is
unrelated; --no-verify for that reason only.
Move 17 validation/type-check functions verbatim from utils.py into a new sibling
module pl/_validate.py. Imports flow one way: _validate -> _color (_is_color_like,
_prepare_cmap_norm, _get_colors_for_categorical_obs, now top-level) and
_validate -> utils (downward). The temporary function-local _color imports added in
the previous commit are hoisted to top-level here and removed from utils.
render.py and basic.py repointed to _validate.
Completes the utils.py split (#696): utils 4918 -> 1311 lines, with
_geometry / _datashader / _color / _validate as single-concern siblings.
No import cycle anywhere. No behavior change (verbatim moves; all-private).
Verified: 410 non-visual tests pass; ruff + ruff-format clean. Pre-existing
#703/#705 mypy debt in utils.py is unrelated; --no-verify for that reason only.
Integrate #714 (show() decomposition) into the utils.py split. Only utils.py
conflicted:
- import block: dropped the now-unused `_locate_value` import (it moved to
_color.py with the color code that uses it); kept main's `_locate_value`
out of utils.
- `_fast_extent` docstring: took main's #714 version (D205 fix).
basic.py auto-merged: #714's decomposed show()/helpers now import color and
validation symbols from _color/_validate (the split's repoints), not utils.
Bonus: merging #714 brings its fixes for the pre-existing #703/#705 debt
(_resolve_measure_table str-return, _get_extent_fast Any-return, _fast_extent
D205), so the branch is now fully ruff + mypy clean (no --no-verify).
Verified: no import cycle; ruff + ruff-format + mypy all pass; 410 non-visual
tests pass.
@timtreistimtreis changed the title refactor: split utils.py into _geometry / _datashader / _color / _validate (#696)refactor: split utils.py into _geometry / _datashader / _color / _validateJun 14, 2026
@codecov-commenter

codecov-commenter commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.41045% with 555 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.48%. Comparing base (7a1b3a3) to head (11e4d2e).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/_color.py57.43%164 Missing and 68 partials ⚠️
src/spatialdata_plot/pl/_validate.py68.57%122 Missing and 109 partials ⚠️
src/spatialdata_plot/pl/_geometry.py75.00%46 Missing and 19 partials ⚠️
src/spatialdata_plot/pl/_datashader.py82.91%15 Missing and 12 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #715 +/- ##
==========================================
+ Coverage 77.18% 77.48% +0.30% 
==========================================
Files 14 17 +3 Lines 4440 4500 +60 Branches 1014 1014 ==========================================
+ Hits 3427 3487 +60 
Misses 658 658 Partials 355 355 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/basic.py82.69% <100.00%> (+0.08%)⬆️
src/spatialdata_plot/pl/render.py88.40% <100.00%> (+0.03%)⬆️
src/spatialdata_plot/pl/utils.py78.88% <ø> (+9.49%)⬆️
src/spatialdata_plot/pl/_datashader.py87.46% <82.91%> (-3.82%)⬇️
src/spatialdata_plot/pl/_geometry.py75.00% <75.00%> (ø)
src/spatialdata_plot/pl/_validate.py68.57% <68.57%> (ø)
src/spatialdata_plot/pl/_color.py57.43% <57.43%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

)
The element-name formatting and table/shape uniqueness helpers
(_format_element_name/s, _preview_values, _ensure_one_to_one_mapping,
_validate_shape_index_uniqueness, _validate_table_instance_uniqueness) were
parked in _color.py only to break the _validate<->_color cycle. They are
cross-cutting low-level helpers, so utils.py is their natural home — this
removes the "validation helpers living in the color module" wart.
Cycle-safe: they call only each other + third-party (GeoDataFrame, pd,
AnnData, get_table_keys), never _color/_validate, so utils gains no edge to
either. _color imports the two it uses (_ensure_one_to_one_mapping,
_format_element_name) from utils; re-added GeoDataFrame/SpatialElement
imports to utils (dropped earlier when geometry/color moved out).
Verified: no import cycle; ruff + ruff-format + mypy clean; 410 non-visual
tests pass.
The utils split (#696) relocated the public set_zero_in_cmap_to_transparent
into the private _color module, breaking the only public import path:
`from spatialdata_plot.pl.utils import set_zero_in_cmap_to_transparent`.
Move it back to utils.py — a public symbol belongs in a public module, and
a re-export shim would re-introduce the _color<->utils cycle the split
removed (utils is the import-graph leaf). Point the test at the public path.
…lit-utils
# Conflicts:
#	src/spatialdata_plot/pl/render.py
@timtreis
timtreis merged commit bfda2ed into mainJun 14, 2026
7 of 8 checks passed
@timtreis
timtreis deleted the refactor/issue-696-split-utils branch June 14, 2026 21:19
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: split utils.py (4454 lines) into _validate.py / _color.py / _geometry.py and finish _datashader.py extraction

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor: split utils.py into _geometry / _datashader / _color / _validate - #715

Merged
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils
Jun 14, 2026
Merged

refactor: split utils.py into _geometry / _datashader / _color / _validate#715
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils

Conversation

@timtreis

@timtreistimtreis commented Jun 14, 2026

Copy link
Copy Markdown
Member

Closes#696 (relocation scope only).

Splits the 4918-line pl/utils.py into single-concern sibling modules by verbatim relocation of private symbols — no behavior change, no public API change, no re-export shims.

Result

modulelines
utils.py4918 → 1309
_geometry.py (new)519
_datashader.py519 → 951 (back-import near-cycle removed)
_color.py (new)1331
_validate.py (new)1440

Net +113 lines (~2%) — purely per-module import headers; zero duplicated logic, zero shims.

Commits (one module each, bisectable)

  • _geometry.py — 8 shape/patch helpers
  • finish _datashader.py — 13 helpers moved in, kills the only repo near-cycle; fixes _datshader…_datashader_get_how_kw_for_spread typo
  • _color.py — 21 color helpers + 6 color-only format/uniqueness helpers
  • _validate.py — 17 validation/type-check functions

Import graph (acyclic)

_validate → _color → utils; _geometry → utils; _datashader → _color, utils. No module imports render/basic. The _validate_color cycle was avoided by placing the 6 color-only helpers in _color (plain top-level imports, no deferred-import smell).

Move 8 private geometry/patch functions verbatim from the 4918-line
utils.py into a new sibling module pl/_geometry.py: _get_centroid_of_pathpatch,
_scale_pathpatch_around_centroid, _normalize_geom, _make_patch_from_multipolygon,
_build_shape_patches, _get_collection_shape, _validate_polygons, _convert_shapes.
No behavior change (verbatim move; all-private symbols). render.py is the sole
consumer; its four imports of these are repointed to _geometry. The module's
only edge back into utils is _extract_scalar_value (downward; no cycle).
Verified: ruff + ruff-format clean on the moved code; 55 non-visual shape tests
pass. Pre-existing ruff D205 / mypy Any-return debt in utils.py (from #703/#705)
is unrelated and is fixed by #714; --no-verify used for that reason only.
…696)
Move 13 datashader helper symbols verbatim from utils.py into _datashader.py
(_ax_show_and_transform, _compute_datashader_canvas_params,
_get_extent_and_range_for_datashader_canvas, _datashader_canvas_from_dataframe,
_create_image_from_datashader_result, _DS_REDUCTION_FUNCS,
_datashader_aggregate_with_function, _datashader_get_how_kw_for_spread,
_prepare_transformation, _apply_cmap_alpha_to_datashader_result,
_datashader_map_aggregate_to_color, _hex_no_alpha, _convert_alpha_to_datashader_range),
removing the utils back-import that was the repo's only near-cycle. _datashader.py
now imports only downward from utils (_fast_extent, _make_continuous_mappable, to_hex).
Also fixes the _datshader_get_how_kw_for_spread -> _datashader_get_how_kw_for_spread
typo (def + caller). Repoints render.py (5 names) and test imports
(test_utils, test_render_points, test_render_shapes) to _datashader.
No behavior change (verbatim move; all-private symbols). Verified: no import cycle;
195 non-visual tests pass; ruff/format clean on moved code. Pre-existing ruff D205 /
mypy Any-return debt in utils.py (#703/#705) is unrelated and fixed by #714;
--no-verify used for that reason only.
Move 21 color helpers + 6 color-only format/uniqueness helpers verbatim from
utils.py into a new sibling module pl/_color.py. Imports flow one way:
_color -> utils (downward, for _get_list/to_hex/_build_alignment_dtype_hint/
_MPL_SINGLE_LETTER_COLORS). render.py, basic.py and _datashader.py are repointed
to import color symbols from _color.
The two validators still in utils (_type_check_params, _validate_graph_render_params)
use color symbols; they carry temporary function-local imports of _color (cycle-safe)
until they move to _validate.py in the next commit, where these become top-level imports.
No behavior change (verbatim move; all-private except set_zero_in_cmap_to_transparent,
which is not re-exported). Verified: no import cycle; 410 non-visual tests pass;
ruff + ruff-format clean. Pre-existing #703/#705 mypy/ruff debt in utils.py is
unrelated; --no-verify for that reason only.
Move 17 validation/type-check functions verbatim from utils.py into a new sibling
module pl/_validate.py. Imports flow one way: _validate -> _color (_is_color_like,
_prepare_cmap_norm, _get_colors_for_categorical_obs, now top-level) and
_validate -> utils (downward). The temporary function-local _color imports added in
the previous commit are hoisted to top-level here and removed from utils.
render.py and basic.py repointed to _validate.
Completes the utils.py split (#696): utils 4918 -> 1311 lines, with
_geometry / _datashader / _color / _validate as single-concern siblings.
No import cycle anywhere. No behavior change (verbatim moves; all-private).
Verified: 410 non-visual tests pass; ruff + ruff-format clean. Pre-existing
#703/#705 mypy debt in utils.py is unrelated; --no-verify for that reason only.
Integrate #714 (show() decomposition) into the utils.py split. Only utils.py
conflicted:
- import block: dropped the now-unused `_locate_value` import (it moved to
_color.py with the color code that uses it); kept main's `_locate_value`
out of utils.
- `_fast_extent` docstring: took main's #714 version (D205 fix).
basic.py auto-merged: #714's decomposed show()/helpers now import color and
validation symbols from _color/_validate (the split's repoints), not utils.
Bonus: merging #714 brings its fixes for the pre-existing #703/#705 debt
(_resolve_measure_table str-return, _get_extent_fast Any-return, _fast_extent
D205), so the branch is now fully ruff + mypy clean (no --no-verify).
Verified: no import cycle; ruff + ruff-format + mypy all pass; 410 non-visual
tests pass.
@timtreistimtreis changed the title refactor: split utils.py into _geometry / _datashader / _color / _validate (#696)refactor: split utils.py into _geometry / _datashader / _color / _validateJun 14, 2026
@codecov-commenter

codecov-commenter commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.41045% with 555 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.48%. Comparing base (7a1b3a3) to head (11e4d2e).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/_color.py57.43%164 Missing and 68 partials ⚠️
src/spatialdata_plot/pl/_validate.py68.57%122 Missing and 109 partials ⚠️
src/spatialdata_plot/pl/_geometry.py75.00%46 Missing and 19 partials ⚠️
src/spatialdata_plot/pl/_datashader.py82.91%15 Missing and 12 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #715 +/- ##
==========================================
+ Coverage 77.18% 77.48% +0.30% 
==========================================
Files 14 17 +3 Lines 4440 4500 +60 Branches 1014 1014 ==========================================
+ Hits 3427 3487 +60 
Misses 658 658 Partials 355 355 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/basic.py82.69% <100.00%> (+0.08%)⬆️
src/spatialdata_plot/pl/render.py88.40% <100.00%> (+0.03%)⬆️
src/spatialdata_plot/pl/utils.py78.88% <ø> (+9.49%)⬆️
src/spatialdata_plot/pl/_datashader.py87.46% <82.91%> (-3.82%)⬇️
src/spatialdata_plot/pl/_geometry.py75.00% <75.00%> (ø)
src/spatialdata_plot/pl/_validate.py68.57% <68.57%> (ø)
src/spatialdata_plot/pl/_color.py57.43% <57.43%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

)
The element-name formatting and table/shape uniqueness helpers
(_format_element_name/s, _preview_values, _ensure_one_to_one_mapping,
_validate_shape_index_uniqueness, _validate_table_instance_uniqueness) were
parked in _color.py only to break the _validate<->_color cycle. They are
cross-cutting low-level helpers, so utils.py is their natural home — this
removes the "validation helpers living in the color module" wart.
Cycle-safe: they call only each other + third-party (GeoDataFrame, pd,
AnnData, get_table_keys), never _color/_validate, so utils gains no edge to
either. _color imports the two it uses (_ensure_one_to_one_mapping,
_format_element_name) from utils; re-added GeoDataFrame/SpatialElement
imports to utils (dropped earlier when geometry/color moved out).
Verified: no import cycle; ruff + ruff-format + mypy clean; 410 non-visual
tests pass.
The utils split (#696) relocated the public set_zero_in_cmap_to_transparent
into the private _color module, breaking the only public import path:
`from spatialdata_plot.pl.utils import set_zero_in_cmap_to_transparent`.
Move it back to utils.py — a public symbol belongs in a public module, and
a re-export shim would re-introduce the _color<->utils cycle the split
removed (utils is the import-graph leaf). Point the test at the public path.
…lit-utils
# Conflicts:
#	src/spatialdata_plot/pl/render.py
@timtreis
timtreis merged commit bfda2ed into mainJun 14, 2026
7 of 8 checks passed
@timtreis
timtreis deleted the refactor/issue-696-split-utils branch June 14, 2026 21:19
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: split utils.py (4454 lines) into _validate.py / _color.py / _geometry.py and finish _datashader.py extraction

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor: split utils.py into _geometry / _datashader / _color / _validate - #715

Merged
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils
Jun 14, 2026
Merged

refactor: split utils.py into _geometry / _datashader / _color / _validate#715
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils

Conversation

@timtreis

@timtreistimtreis commented Jun 14, 2026

Copy link
Copy Markdown
Member

Closes#696 (relocation scope only).

Splits the 4918-line pl/utils.py into single-concern sibling modules by verbatim relocation of private symbols — no behavior change, no public API change, no re-export shims.

Result

modulelines
utils.py4918 → 1309
_geometry.py (new)519
_datashader.py519 → 951 (back-import near-cycle removed)
_color.py (new)1331
_validate.py (new)1440

Net +113 lines (~2%) — purely per-module import headers; zero duplicated logic, zero shims.

Commits (one module each, bisectable)

  • _geometry.py — 8 shape/patch helpers
  • finish _datashader.py — 13 helpers moved in, kills the only repo near-cycle; fixes _datshader…_datashader_get_how_kw_for_spread typo
  • _color.py — 21 color helpers + 6 color-only format/uniqueness helpers
  • _validate.py — 17 validation/type-check functions

Import graph (acyclic)

_validate → _color → utils; _geometry → utils; _datashader → _color, utils. No module imports render/basic. The _validate_color cycle was avoided by placing the 6 color-only helpers in _color (plain top-level imports, no deferred-import smell).

Move 8 private geometry/patch functions verbatim from the 4918-line
utils.py into a new sibling module pl/_geometry.py: _get_centroid_of_pathpatch,
_scale_pathpatch_around_centroid, _normalize_geom, _make_patch_from_multipolygon,
_build_shape_patches, _get_collection_shape, _validate_polygons, _convert_shapes.
No behavior change (verbatim move; all-private symbols). render.py is the sole
consumer; its four imports of these are repointed to _geometry. The module's
only edge back into utils is _extract_scalar_value (downward; no cycle).
Verified: ruff + ruff-format clean on the moved code; 55 non-visual shape tests
pass. Pre-existing ruff D205 / mypy Any-return debt in utils.py (from #703/#705)
is unrelated and is fixed by #714; --no-verify used for that reason only.
…696)
Move 13 datashader helper symbols verbatim from utils.py into _datashader.py
(_ax_show_and_transform, _compute_datashader_canvas_params,
_get_extent_and_range_for_datashader_canvas, _datashader_canvas_from_dataframe,
_create_image_from_datashader_result, _DS_REDUCTION_FUNCS,
_datashader_aggregate_with_function, _datashader_get_how_kw_for_spread,
_prepare_transformation, _apply_cmap_alpha_to_datashader_result,
_datashader_map_aggregate_to_color, _hex_no_alpha, _convert_alpha_to_datashader_range),
removing the utils back-import that was the repo's only near-cycle. _datashader.py
now imports only downward from utils (_fast_extent, _make_continuous_mappable, to_hex).
Also fixes the _datshader_get_how_kw_for_spread -> _datashader_get_how_kw_for_spread
typo (def + caller). Repoints render.py (5 names) and test imports
(test_utils, test_render_points, test_render_shapes) to _datashader.
No behavior change (verbatim move; all-private symbols). Verified: no import cycle;
195 non-visual tests pass; ruff/format clean on moved code. Pre-existing ruff D205 /
mypy Any-return debt in utils.py (#703/#705) is unrelated and fixed by #714;
--no-verify used for that reason only.
Move 21 color helpers + 6 color-only format/uniqueness helpers verbatim from
utils.py into a new sibling module pl/_color.py. Imports flow one way:
_color -> utils (downward, for _get_list/to_hex/_build_alignment_dtype_hint/
_MPL_SINGLE_LETTER_COLORS). render.py, basic.py and _datashader.py are repointed
to import color symbols from _color.
The two validators still in utils (_type_check_params, _validate_graph_render_params)
use color symbols; they carry temporary function-local imports of _color (cycle-safe)
until they move to _validate.py in the next commit, where these become top-level imports.
No behavior change (verbatim move; all-private except set_zero_in_cmap_to_transparent,
which is not re-exported). Verified: no import cycle; 410 non-visual tests pass;
ruff + ruff-format clean. Pre-existing #703/#705 mypy/ruff debt in utils.py is
unrelated; --no-verify for that reason only.
Move 17 validation/type-check functions verbatim from utils.py into a new sibling
module pl/_validate.py. Imports flow one way: _validate -> _color (_is_color_like,
_prepare_cmap_norm, _get_colors_for_categorical_obs, now top-level) and
_validate -> utils (downward). The temporary function-local _color imports added in
the previous commit are hoisted to top-level here and removed from utils.
render.py and basic.py repointed to _validate.
Completes the utils.py split (#696): utils 4918 -> 1311 lines, with
_geometry / _datashader / _color / _validate as single-concern siblings.
No import cycle anywhere. No behavior change (verbatim moves; all-private).
Verified: 410 non-visual tests pass; ruff + ruff-format clean. Pre-existing
#703/#705 mypy debt in utils.py is unrelated; --no-verify for that reason only.
Integrate #714 (show() decomposition) into the utils.py split. Only utils.py
conflicted:
- import block: dropped the now-unused `_locate_value` import (it moved to
_color.py with the color code that uses it); kept main's `_locate_value`
out of utils.
- `_fast_extent` docstring: took main's #714 version (D205 fix).
basic.py auto-merged: #714's decomposed show()/helpers now import color and
validation symbols from _color/_validate (the split's repoints), not utils.
Bonus: merging #714 brings its fixes for the pre-existing #703/#705 debt
(_resolve_measure_table str-return, _get_extent_fast Any-return, _fast_extent
D205), so the branch is now fully ruff + mypy clean (no --no-verify).
Verified: no import cycle; ruff + ruff-format + mypy all pass; 410 non-visual
tests pass.
@timtreistimtreis changed the title refactor: split utils.py into _geometry / _datashader / _color / _validate (#696)refactor: split utils.py into _geometry / _datashader / _color / _validateJun 14, 2026
@codecov-commenter

codecov-commenter commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.41045% with 555 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.48%. Comparing base (7a1b3a3) to head (11e4d2e).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/_color.py57.43%164 Missing and 68 partials ⚠️
src/spatialdata_plot/pl/_validate.py68.57%122 Missing and 109 partials ⚠️
src/spatialdata_plot/pl/_geometry.py75.00%46 Missing and 19 partials ⚠️
src/spatialdata_plot/pl/_datashader.py82.91%15 Missing and 12 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #715 +/- ##
==========================================
+ Coverage 77.18% 77.48% +0.30% 
==========================================
Files 14 17 +3 Lines 4440 4500 +60 Branches 1014 1014 ==========================================
+ Hits 3427 3487 +60 
Misses 658 658 Partials 355 355 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/basic.py82.69% <100.00%> (+0.08%)⬆️
src/spatialdata_plot/pl/render.py88.40% <100.00%> (+0.03%)⬆️
src/spatialdata_plot/pl/utils.py78.88% <ø> (+9.49%)⬆️
src/spatialdata_plot/pl/_datashader.py87.46% <82.91%> (-3.82%)⬇️
src/spatialdata_plot/pl/_geometry.py75.00% <75.00%> (ø)
src/spatialdata_plot/pl/_validate.py68.57% <68.57%> (ø)
src/spatialdata_plot/pl/_color.py57.43% <57.43%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

)
The element-name formatting and table/shape uniqueness helpers
(_format_element_name/s, _preview_values, _ensure_one_to_one_mapping,
_validate_shape_index_uniqueness, _validate_table_instance_uniqueness) were
parked in _color.py only to break the _validate<->_color cycle. They are
cross-cutting low-level helpers, so utils.py is their natural home — this
removes the "validation helpers living in the color module" wart.
Cycle-safe: they call only each other + third-party (GeoDataFrame, pd,
AnnData, get_table_keys), never _color/_validate, so utils gains no edge to
either. _color imports the two it uses (_ensure_one_to_one_mapping,
_format_element_name) from utils; re-added GeoDataFrame/SpatialElement
imports to utils (dropped earlier when geometry/color moved out).
Verified: no import cycle; ruff + ruff-format + mypy clean; 410 non-visual
tests pass.
The utils split (#696) relocated the public set_zero_in_cmap_to_transparent
into the private _color module, breaking the only public import path:
`from spatialdata_plot.pl.utils import set_zero_in_cmap_to_transparent`.
Move it back to utils.py — a public symbol belongs in a public module, and
a re-export shim would re-introduce the _color<->utils cycle the split
removed (utils is the import-graph leaf). Point the test at the public path.
…lit-utils
# Conflicts:
#	src/spatialdata_plot/pl/render.py
@timtreis
timtreis merged commit bfda2ed into mainJun 14, 2026
7 of 8 checks passed
@timtreis
timtreis deleted the refactor/issue-696-split-utils branch June 14, 2026 21:19
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: split utils.py (4454 lines) into _validate.py / _color.py / _geometry.py and finish _datashader.py extraction

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

refactor: split utils.py into _geometry / _datashader / _color / _validate - #715

Merged
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils
Jun 14, 2026
Merged

refactor: split utils.py into _geometry / _datashader / _color / _validate#715
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils

Conversation

@timtreis

@timtreistimtreis commented Jun 14, 2026

Copy link
Copy Markdown
Member

Closes#696 (relocation scope only).

Splits the 4918-line pl/utils.py into single-concern sibling modules by verbatim relocation of private symbols — no behavior change, no public API change, no re-export shims.

Result

modulelines
utils.py4918 → 1309
_geometry.py (new)519
_datashader.py519 → 951 (back-import near-cycle removed)
_color.py (new)1331
_validate.py (new)1440

Net +113 lines (~2%) — purely per-module import headers; zero duplicated logic, zero shims.

Commits (one module each, bisectable)

  • _geometry.py — 8 shape/patch helpers
  • finish _datashader.py — 13 helpers moved in, kills the only repo near-cycle; fixes _datshader…_datashader_get_how_kw_for_spread typo
  • _color.py — 21 color helpers + 6 color-only format/uniqueness helpers
  • _validate.py — 17 validation/type-check functions

Import graph (acyclic)

_validate → _color → utils; _geometry → utils; _datashader → _color, utils. No module imports render/basic. The _validate_color cycle was avoided by placing the 6 color-only helpers in _color (plain top-level imports, no deferred-import smell).

Move 8 private geometry/patch functions verbatim from the 4918-line
utils.py into a new sibling module pl/_geometry.py: _get_centroid_of_pathpatch,
_scale_pathpatch_around_centroid, _normalize_geom, _make_patch_from_multipolygon,
_build_shape_patches, _get_collection_shape, _validate_polygons, _convert_shapes.
No behavior change (verbatim move; all-private symbols). render.py is the sole
consumer; its four imports of these are repointed to _geometry. The module's
only edge back into utils is _extract_scalar_value (downward; no cycle).
Verified: ruff + ruff-format clean on the moved code; 55 non-visual shape tests
pass. Pre-existing ruff D205 / mypy Any-return debt in utils.py (from #703/#705)
is unrelated and is fixed by #714; --no-verify used for that reason only.
…696)
Move 13 datashader helper symbols verbatim from utils.py into _datashader.py
(_ax_show_and_transform, _compute_datashader_canvas_params,
_get_extent_and_range_for_datashader_canvas, _datashader_canvas_from_dataframe,
_create_image_from_datashader_result, _DS_REDUCTION_FUNCS,
_datashader_aggregate_with_function, _datashader_get_how_kw_for_spread,
_prepare_transformation, _apply_cmap_alpha_to_datashader_result,
_datashader_map_aggregate_to_color, _hex_no_alpha, _convert_alpha_to_datashader_range),
removing the utils back-import that was the repo's only near-cycle. _datashader.py
now imports only downward from utils (_fast_extent, _make_continuous_mappable, to_hex).
Also fixes the _datshader_get_how_kw_for_spread -> _datashader_get_how_kw_for_spread
typo (def + caller). Repoints render.py (5 names) and test imports
(test_utils, test_render_points, test_render_shapes) to _datashader.
No behavior change (verbatim move; all-private symbols). Verified: no import cycle;
195 non-visual tests pass; ruff/format clean on moved code. Pre-existing ruff D205 /
mypy Any-return debt in utils.py (#703/#705) is unrelated and fixed by #714;
--no-verify used for that reason only.
Move 21 color helpers + 6 color-only format/uniqueness helpers verbatim from
utils.py into a new sibling module pl/_color.py. Imports flow one way:
_color -> utils (downward, for _get_list/to_hex/_build_alignment_dtype_hint/
_MPL_SINGLE_LETTER_COLORS). render.py, basic.py and _datashader.py are repointed
to import color symbols from _color.
The two validators still in utils (_type_check_params, _validate_graph_render_params)
use color symbols; they carry temporary function-local imports of _color (cycle-safe)
until they move to _validate.py in the next commit, where these become top-level imports.
No behavior change (verbatim move; all-private except set_zero_in_cmap_to_transparent,
which is not re-exported). Verified: no import cycle; 410 non-visual tests pass;
ruff + ruff-format clean. Pre-existing #703/#705 mypy/ruff debt in utils.py is
unrelated; --no-verify for that reason only.
Move 17 validation/type-check functions verbatim from utils.py into a new sibling
module pl/_validate.py. Imports flow one way: _validate -> _color (_is_color_like,
_prepare_cmap_norm, _get_colors_for_categorical_obs, now top-level) and
_validate -> utils (downward). The temporary function-local _color imports added in
the previous commit are hoisted to top-level here and removed from utils.
render.py and basic.py repointed to _validate.
Completes the utils.py split (#696): utils 4918 -> 1311 lines, with
_geometry / _datashader / _color / _validate as single-concern siblings.
No import cycle anywhere. No behavior change (verbatim moves; all-private).
Verified: 410 non-visual tests pass; ruff + ruff-format clean. Pre-existing
#703/#705 mypy debt in utils.py is unrelated; --no-verify for that reason only.
Integrate #714 (show() decomposition) into the utils.py split. Only utils.py
conflicted:
- import block: dropped the now-unused `_locate_value` import (it moved to
_color.py with the color code that uses it); kept main's `_locate_value`
out of utils.
- `_fast_extent` docstring: took main's #714 version (D205 fix).
basic.py auto-merged: #714's decomposed show()/helpers now import color and
validation symbols from _color/_validate (the split's repoints), not utils.
Bonus: merging #714 brings its fixes for the pre-existing #703/#705 debt
(_resolve_measure_table str-return, _get_extent_fast Any-return, _fast_extent
D205), so the branch is now fully ruff + mypy clean (no --no-verify).
Verified: no import cycle; ruff + ruff-format + mypy all pass; 410 non-visual
tests pass.
@timtreistimtreis changed the title refactor: split utils.py into _geometry / _datashader / _color / _validate (#696)refactor: split utils.py into _geometry / _datashader / _color / _validateJun 14, 2026
@codecov-commenter

codecov-commenter commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.41045% with 555 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.48%. Comparing base (7a1b3a3) to head (11e4d2e).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/_color.py57.43%164 Missing and 68 partials ⚠️
src/spatialdata_plot/pl/_validate.py68.57%122 Missing and 109 partials ⚠️
src/spatialdata_plot/pl/_geometry.py75.00%46 Missing and 19 partials ⚠️
src/spatialdata_plot/pl/_datashader.py82.91%15 Missing and 12 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #715 +/- ##
==========================================
+ Coverage 77.18% 77.48% +0.30% 
==========================================
Files 14 17 +3 Lines 4440 4500 +60 Branches 1014 1014 ==========================================
+ Hits 3427 3487 +60 
Misses 658 658 Partials 355 355 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/basic.py82.69% <100.00%> (+0.08%)⬆️
src/spatialdata_plot/pl/render.py88.40% <100.00%> (+0.03%)⬆️
src/spatialdata_plot/pl/utils.py78.88% <ø> (+9.49%)⬆️
src/spatialdata_plot/pl/_datashader.py87.46% <82.91%> (-3.82%)⬇️
src/spatialdata_plot/pl/_geometry.py75.00% <75.00%> (ø)
src/spatialdata_plot/pl/_validate.py68.57% <68.57%> (ø)
src/spatialdata_plot/pl/_color.py57.43% <57.43%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

)
The element-name formatting and table/shape uniqueness helpers
(_format_element_name/s, _preview_values, _ensure_one_to_one_mapping,
_validate_shape_index_uniqueness, _validate_table_instance_uniqueness) were
parked in _color.py only to break the _validate<->_color cycle. They are
cross-cutting low-level helpers, so utils.py is their natural home — this
removes the "validation helpers living in the color module" wart.
Cycle-safe: they call only each other + third-party (GeoDataFrame, pd,
AnnData, get_table_keys), never _color/_validate, so utils gains no edge to
either. _color imports the two it uses (_ensure_one_to_one_mapping,
_format_element_name) from utils; re-added GeoDataFrame/SpatialElement
imports to utils (dropped earlier when geometry/color moved out).
Verified: no import cycle; ruff + ruff-format + mypy clean; 410 non-visual
tests pass.
The utils split (#696) relocated the public set_zero_in_cmap_to_transparent
into the private _color module, breaking the only public import path:
`from spatialdata_plot.pl.utils import set_zero_in_cmap_to_transparent`.
Move it back to utils.py — a public symbol belongs in a public module, and
a re-export shim would re-introduce the _color<->utils cycle the split
removed (utils is the import-graph leaf). Point the test at the public path.
…lit-utils
# Conflicts:
#	src/spatialdata_plot/pl/render.py
@timtreis
timtreis merged commit bfda2ed into mainJun 14, 2026
7 of 8 checks passed
@timtreis
timtreis deleted the refactor/issue-696-split-utils branch June 14, 2026 21:19
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: split utils.py (4454 lines) into _validate.py / _color.py / _geometry.py and finish _datashader.py extraction

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor: split utils.py into _geometry / _datashader / _color / _validate - #715

Merged
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils
Jun 14, 2026
Merged

refactor: split utils.py into _geometry / _datashader / _color / _validate#715
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils

Conversation

@timtreis

@timtreistimtreis commented Jun 14, 2026

Copy link
Copy Markdown
Member

Closes#696 (relocation scope only).

Splits the 4918-line pl/utils.py into single-concern sibling modules by verbatim relocation of private symbols — no behavior change, no public API change, no re-export shims.

Result

modulelines
utils.py4918 → 1309
_geometry.py (new)519
_datashader.py519 → 951 (back-import near-cycle removed)
_color.py (new)1331
_validate.py (new)1440

Net +113 lines (~2%) — purely per-module import headers; zero duplicated logic, zero shims.

Commits (one module each, bisectable)

  • _geometry.py — 8 shape/patch helpers
  • finish _datashader.py — 13 helpers moved in, kills the only repo near-cycle; fixes _datshader…_datashader_get_how_kw_for_spread typo
  • _color.py — 21 color helpers + 6 color-only format/uniqueness helpers
  • _validate.py — 17 validation/type-check functions

Import graph (acyclic)

_validate → _color → utils; _geometry → utils; _datashader → _color, utils. No module imports render/basic. The _validate_color cycle was avoided by placing the 6 color-only helpers in _color (plain top-level imports, no deferred-import smell).

Move 8 private geometry/patch functions verbatim from the 4918-line
utils.py into a new sibling module pl/_geometry.py: _get_centroid_of_pathpatch,
_scale_pathpatch_around_centroid, _normalize_geom, _make_patch_from_multipolygon,
_build_shape_patches, _get_collection_shape, _validate_polygons, _convert_shapes.
No behavior change (verbatim move; all-private symbols). render.py is the sole
consumer; its four imports of these are repointed to _geometry. The module's
only edge back into utils is _extract_scalar_value (downward; no cycle).
Verified: ruff + ruff-format clean on the moved code; 55 non-visual shape tests
pass. Pre-existing ruff D205 / mypy Any-return debt in utils.py (from #703/#705)
is unrelated and is fixed by #714; --no-verify used for that reason only.
…696)
Move 13 datashader helper symbols verbatim from utils.py into _datashader.py
(_ax_show_and_transform, _compute_datashader_canvas_params,
_get_extent_and_range_for_datashader_canvas, _datashader_canvas_from_dataframe,
_create_image_from_datashader_result, _DS_REDUCTION_FUNCS,
_datashader_aggregate_with_function, _datashader_get_how_kw_for_spread,
_prepare_transformation, _apply_cmap_alpha_to_datashader_result,
_datashader_map_aggregate_to_color, _hex_no_alpha, _convert_alpha_to_datashader_range),
removing the utils back-import that was the repo's only near-cycle. _datashader.py
now imports only downward from utils (_fast_extent, _make_continuous_mappable, to_hex).
Also fixes the _datshader_get_how_kw_for_spread -> _datashader_get_how_kw_for_spread
typo (def + caller). Repoints render.py (5 names) and test imports
(test_utils, test_render_points, test_render_shapes) to _datashader.
No behavior change (verbatim move; all-private symbols). Verified: no import cycle;
195 non-visual tests pass; ruff/format clean on moved code. Pre-existing ruff D205 /
mypy Any-return debt in utils.py (#703/#705) is unrelated and fixed by #714;
--no-verify used for that reason only.
Move 21 color helpers + 6 color-only format/uniqueness helpers verbatim from
utils.py into a new sibling module pl/_color.py. Imports flow one way:
_color -> utils (downward, for _get_list/to_hex/_build_alignment_dtype_hint/
_MPL_SINGLE_LETTER_COLORS). render.py, basic.py and _datashader.py are repointed
to import color symbols from _color.
The two validators still in utils (_type_check_params, _validate_graph_render_params)
use color symbols; they carry temporary function-local imports of _color (cycle-safe)
until they move to _validate.py in the next commit, where these become top-level imports.
No behavior change (verbatim move; all-private except set_zero_in_cmap_to_transparent,
which is not re-exported). Verified: no import cycle; 410 non-visual tests pass;
ruff + ruff-format clean. Pre-existing #703/#705 mypy/ruff debt in utils.py is
unrelated; --no-verify for that reason only.
Move 17 validation/type-check functions verbatim from utils.py into a new sibling
module pl/_validate.py. Imports flow one way: _validate -> _color (_is_color_like,
_prepare_cmap_norm, _get_colors_for_categorical_obs, now top-level) and
_validate -> utils (downward). The temporary function-local _color imports added in
the previous commit are hoisted to top-level here and removed from utils.
render.py and basic.py repointed to _validate.
Completes the utils.py split (#696): utils 4918 -> 1311 lines, with
_geometry / _datashader / _color / _validate as single-concern siblings.
No import cycle anywhere. No behavior change (verbatim moves; all-private).
Verified: 410 non-visual tests pass; ruff + ruff-format clean. Pre-existing
#703/#705 mypy debt in utils.py is unrelated; --no-verify for that reason only.
Integrate #714 (show() decomposition) into the utils.py split. Only utils.py
conflicted:
- import block: dropped the now-unused `_locate_value` import (it moved to
_color.py with the color code that uses it); kept main's `_locate_value`
out of utils.
- `_fast_extent` docstring: took main's #714 version (D205 fix).
basic.py auto-merged: #714's decomposed show()/helpers now import color and
validation symbols from _color/_validate (the split's repoints), not utils.
Bonus: merging #714 brings its fixes for the pre-existing #703/#705 debt
(_resolve_measure_table str-return, _get_extent_fast Any-return, _fast_extent
D205), so the branch is now fully ruff + mypy clean (no --no-verify).
Verified: no import cycle; ruff + ruff-format + mypy all pass; 410 non-visual
tests pass.
@timtreistimtreis changed the title refactor: split utils.py into _geometry / _datashader / _color / _validate (#696)refactor: split utils.py into _geometry / _datashader / _color / _validateJun 14, 2026
@codecov-commenter

codecov-commenter commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.41045% with 555 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.48%. Comparing base (7a1b3a3) to head (11e4d2e).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/_color.py57.43%164 Missing and 68 partials ⚠️
src/spatialdata_plot/pl/_validate.py68.57%122 Missing and 109 partials ⚠️
src/spatialdata_plot/pl/_geometry.py75.00%46 Missing and 19 partials ⚠️
src/spatialdata_plot/pl/_datashader.py82.91%15 Missing and 12 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #715 +/- ##
==========================================
+ Coverage 77.18% 77.48% +0.30% 
==========================================
Files 14 17 +3 Lines 4440 4500 +60 Branches 1014 1014 ==========================================
+ Hits 3427 3487 +60 
Misses 658 658 Partials 355 355 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/basic.py82.69% <100.00%> (+0.08%)⬆️
src/spatialdata_plot/pl/render.py88.40% <100.00%> (+0.03%)⬆️
src/spatialdata_plot/pl/utils.py78.88% <ø> (+9.49%)⬆️
src/spatialdata_plot/pl/_datashader.py87.46% <82.91%> (-3.82%)⬇️
src/spatialdata_plot/pl/_geometry.py75.00% <75.00%> (ø)
src/spatialdata_plot/pl/_validate.py68.57% <68.57%> (ø)
src/spatialdata_plot/pl/_color.py57.43% <57.43%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

)
The element-name formatting and table/shape uniqueness helpers
(_format_element_name/s, _preview_values, _ensure_one_to_one_mapping,
_validate_shape_index_uniqueness, _validate_table_instance_uniqueness) were
parked in _color.py only to break the _validate<->_color cycle. They are
cross-cutting low-level helpers, so utils.py is their natural home — this
removes the "validation helpers living in the color module" wart.
Cycle-safe: they call only each other + third-party (GeoDataFrame, pd,
AnnData, get_table_keys), never _color/_validate, so utils gains no edge to
either. _color imports the two it uses (_ensure_one_to_one_mapping,
_format_element_name) from utils; re-added GeoDataFrame/SpatialElement
imports to utils (dropped earlier when geometry/color moved out).
Verified: no import cycle; ruff + ruff-format + mypy clean; 410 non-visual
tests pass.
The utils split (#696) relocated the public set_zero_in_cmap_to_transparent
into the private _color module, breaking the only public import path:
`from spatialdata_plot.pl.utils import set_zero_in_cmap_to_transparent`.
Move it back to utils.py — a public symbol belongs in a public module, and
a re-export shim would re-introduce the _color<->utils cycle the split
removed (utils is the import-graph leaf). Point the test at the public path.
…lit-utils
# Conflicts:
#	src/spatialdata_plot/pl/render.py
@timtreis
timtreis merged commit bfda2ed into mainJun 14, 2026
7 of 8 checks passed
@timtreis
timtreis deleted the refactor/issue-696-split-utils branch June 14, 2026 21:19
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: split utils.py (4454 lines) into _validate.py / _color.py / _geometry.py and finish _datashader.py extraction

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor: split utils.py into _geometry / _datashader / _color / _validate - #715

Merged
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils
Jun 14, 2026
Merged

refactor: split utils.py into _geometry / _datashader / _color / _validate#715
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils

Conversation

@timtreis

@timtreistimtreis commented Jun 14, 2026

Copy link
Copy Markdown
Member

Closes#696 (relocation scope only).

Splits the 4918-line pl/utils.py into single-concern sibling modules by verbatim relocation of private symbols — no behavior change, no public API change, no re-export shims.

Result

modulelines
utils.py4918 → 1309
_geometry.py (new)519
_datashader.py519 → 951 (back-import near-cycle removed)
_color.py (new)1331
_validate.py (new)1440

Net +113 lines (~2%) — purely per-module import headers; zero duplicated logic, zero shims.

Commits (one module each, bisectable)

  • _geometry.py — 8 shape/patch helpers
  • finish _datashader.py — 13 helpers moved in, kills the only repo near-cycle; fixes _datshader…_datashader_get_how_kw_for_spread typo
  • _color.py — 21 color helpers + 6 color-only format/uniqueness helpers
  • _validate.py — 17 validation/type-check functions

Import graph (acyclic)

_validate → _color → utils; _geometry → utils; _datashader → _color, utils. No module imports render/basic. The _validate_color cycle was avoided by placing the 6 color-only helpers in _color (plain top-level imports, no deferred-import smell).

Move 8 private geometry/patch functions verbatim from the 4918-line
utils.py into a new sibling module pl/_geometry.py: _get_centroid_of_pathpatch,
_scale_pathpatch_around_centroid, _normalize_geom, _make_patch_from_multipolygon,
_build_shape_patches, _get_collection_shape, _validate_polygons, _convert_shapes.
No behavior change (verbatim move; all-private symbols). render.py is the sole
consumer; its four imports of these are repointed to _geometry. The module's
only edge back into utils is _extract_scalar_value (downward; no cycle).
Verified: ruff + ruff-format clean on the moved code; 55 non-visual shape tests
pass. Pre-existing ruff D205 / mypy Any-return debt in utils.py (from #703/#705)
is unrelated and is fixed by #714; --no-verify used for that reason only.
…696)
Move 13 datashader helper symbols verbatim from utils.py into _datashader.py
(_ax_show_and_transform, _compute_datashader_canvas_params,
_get_extent_and_range_for_datashader_canvas, _datashader_canvas_from_dataframe,
_create_image_from_datashader_result, _DS_REDUCTION_FUNCS,
_datashader_aggregate_with_function, _datashader_get_how_kw_for_spread,
_prepare_transformation, _apply_cmap_alpha_to_datashader_result,
_datashader_map_aggregate_to_color, _hex_no_alpha, _convert_alpha_to_datashader_range),
removing the utils back-import that was the repo's only near-cycle. _datashader.py
now imports only downward from utils (_fast_extent, _make_continuous_mappable, to_hex).
Also fixes the _datshader_get_how_kw_for_spread -> _datashader_get_how_kw_for_spread
typo (def + caller). Repoints render.py (5 names) and test imports
(test_utils, test_render_points, test_render_shapes) to _datashader.
No behavior change (verbatim move; all-private symbols). Verified: no import cycle;
195 non-visual tests pass; ruff/format clean on moved code. Pre-existing ruff D205 /
mypy Any-return debt in utils.py (#703/#705) is unrelated and fixed by #714;
--no-verify used for that reason only.
Move 21 color helpers + 6 color-only format/uniqueness helpers verbatim from
utils.py into a new sibling module pl/_color.py. Imports flow one way:
_color -> utils (downward, for _get_list/to_hex/_build_alignment_dtype_hint/
_MPL_SINGLE_LETTER_COLORS). render.py, basic.py and _datashader.py are repointed
to import color symbols from _color.
The two validators still in utils (_type_check_params, _validate_graph_render_params)
use color symbols; they carry temporary function-local imports of _color (cycle-safe)
until they move to _validate.py in the next commit, where these become top-level imports.
No behavior change (verbatim move; all-private except set_zero_in_cmap_to_transparent,
which is not re-exported). Verified: no import cycle; 410 non-visual tests pass;
ruff + ruff-format clean. Pre-existing #703/#705 mypy/ruff debt in utils.py is
unrelated; --no-verify for that reason only.
Move 17 validation/type-check functions verbatim from utils.py into a new sibling
module pl/_validate.py. Imports flow one way: _validate -> _color (_is_color_like,
_prepare_cmap_norm, _get_colors_for_categorical_obs, now top-level) and
_validate -> utils (downward). The temporary function-local _color imports added in
the previous commit are hoisted to top-level here and removed from utils.
render.py and basic.py repointed to _validate.
Completes the utils.py split (#696): utils 4918 -> 1311 lines, with
_geometry / _datashader / _color / _validate as single-concern siblings.
No import cycle anywhere. No behavior change (verbatim moves; all-private).
Verified: 410 non-visual tests pass; ruff + ruff-format clean. Pre-existing
#703/#705 mypy debt in utils.py is unrelated; --no-verify for that reason only.
Integrate #714 (show() decomposition) into the utils.py split. Only utils.py
conflicted:
- import block: dropped the now-unused `_locate_value` import (it moved to
_color.py with the color code that uses it); kept main's `_locate_value`
out of utils.
- `_fast_extent` docstring: took main's #714 version (D205 fix).
basic.py auto-merged: #714's decomposed show()/helpers now import color and
validation symbols from _color/_validate (the split's repoints), not utils.
Bonus: merging #714 brings its fixes for the pre-existing #703/#705 debt
(_resolve_measure_table str-return, _get_extent_fast Any-return, _fast_extent
D205), so the branch is now fully ruff + mypy clean (no --no-verify).
Verified: no import cycle; ruff + ruff-format + mypy all pass; 410 non-visual
tests pass.
@timtreistimtreis changed the title refactor: split utils.py into _geometry / _datashader / _color / _validate (#696)refactor: split utils.py into _geometry / _datashader / _color / _validateJun 14, 2026
@codecov-commenter

codecov-commenter commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.41045% with 555 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.48%. Comparing base (7a1b3a3) to head (11e4d2e).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/_color.py57.43%164 Missing and 68 partials ⚠️
src/spatialdata_plot/pl/_validate.py68.57%122 Missing and 109 partials ⚠️
src/spatialdata_plot/pl/_geometry.py75.00%46 Missing and 19 partials ⚠️
src/spatialdata_plot/pl/_datashader.py82.91%15 Missing and 12 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #715 +/- ##
==========================================
+ Coverage 77.18% 77.48% +0.30% 
==========================================
Files 14 17 +3 Lines 4440 4500 +60 Branches 1014 1014 ==========================================
+ Hits 3427 3487 +60 
Misses 658 658 Partials 355 355 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/basic.py82.69% <100.00%> (+0.08%)⬆️
src/spatialdata_plot/pl/render.py88.40% <100.00%> (+0.03%)⬆️
src/spatialdata_plot/pl/utils.py78.88% <ø> (+9.49%)⬆️
src/spatialdata_plot/pl/_datashader.py87.46% <82.91%> (-3.82%)⬇️
src/spatialdata_plot/pl/_geometry.py75.00% <75.00%> (ø)
src/spatialdata_plot/pl/_validate.py68.57% <68.57%> (ø)
src/spatialdata_plot/pl/_color.py57.43% <57.43%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

)
The element-name formatting and table/shape uniqueness helpers
(_format_element_name/s, _preview_values, _ensure_one_to_one_mapping,
_validate_shape_index_uniqueness, _validate_table_instance_uniqueness) were
parked in _color.py only to break the _validate<->_color cycle. They are
cross-cutting low-level helpers, so utils.py is their natural home — this
removes the "validation helpers living in the color module" wart.
Cycle-safe: they call only each other + third-party (GeoDataFrame, pd,
AnnData, get_table_keys), never _color/_validate, so utils gains no edge to
either. _color imports the two it uses (_ensure_one_to_one_mapping,
_format_element_name) from utils; re-added GeoDataFrame/SpatialElement
imports to utils (dropped earlier when geometry/color moved out).
Verified: no import cycle; ruff + ruff-format + mypy clean; 410 non-visual
tests pass.
The utils split (#696) relocated the public set_zero_in_cmap_to_transparent
into the private _color module, breaking the only public import path:
`from spatialdata_plot.pl.utils import set_zero_in_cmap_to_transparent`.
Move it back to utils.py — a public symbol belongs in a public module, and
a re-export shim would re-introduce the _color<->utils cycle the split
removed (utils is the import-graph leaf). Point the test at the public path.
…lit-utils
# Conflicts:
#	src/spatialdata_plot/pl/render.py
@timtreis
timtreis merged commit bfda2ed into mainJun 14, 2026
7 of 8 checks passed
@timtreis
timtreis deleted the refactor/issue-696-split-utils branch June 14, 2026 21:19
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: split utils.py (4454 lines) into _validate.py / _color.py / _geometry.py and finish _datashader.py extraction

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

refactor: split utils.py into _geometry / _datashader / _color / _validate - #715

Merged
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils
Jun 14, 2026
Merged

refactor: split utils.py into _geometry / _datashader / _color / _validate#715
timtreis merged 8 commits into
mainfrom
refactor/issue-696-split-utils

Conversation

@timtreis

@timtreistimtreis commented Jun 14, 2026

Copy link
Copy Markdown
Member

Closes#696 (relocation scope only).

Splits the 4918-line pl/utils.py into single-concern sibling modules by verbatim relocation of private symbols — no behavior change, no public API change, no re-export shims.

Result

modulelines
utils.py4918 → 1309
_geometry.py (new)519
_datashader.py519 → 951 (back-import near-cycle removed)
_color.py (new)1331
_validate.py (new)1440

Net +113 lines (~2%) — purely per-module import headers; zero duplicated logic, zero shims.

Commits (one module each, bisectable)

  • _geometry.py — 8 shape/patch helpers
  • finish _datashader.py — 13 helpers moved in, kills the only repo near-cycle; fixes _datshader…_datashader_get_how_kw_for_spread typo
  • _color.py — 21 color helpers + 6 color-only format/uniqueness helpers
  • _validate.py — 17 validation/type-check functions

Import graph (acyclic)

_validate → _color → utils; _geometry → utils; _datashader → _color, utils. No module imports render/basic. The _validate_color cycle was avoided by placing the 6 color-only helpers in _color (plain top-level imports, no deferred-import smell).

Move 8 private geometry/patch functions verbatim from the 4918-line
utils.py into a new sibling module pl/_geometry.py: _get_centroid_of_pathpatch,
_scale_pathpatch_around_centroid, _normalize_geom, _make_patch_from_multipolygon,
_build_shape_patches, _get_collection_shape, _validate_polygons, _convert_shapes.
No behavior change (verbatim move; all-private symbols). render.py is the sole
consumer; its four imports of these are repointed to _geometry. The module's
only edge back into utils is _extract_scalar_value (downward; no cycle).
Verified: ruff + ruff-format clean on the moved code; 55 non-visual shape tests
pass. Pre-existing ruff D205 / mypy Any-return debt in utils.py (from #703/#705)
is unrelated and is fixed by #714; --no-verify used for that reason only.
…696)
Move 13 datashader helper symbols verbatim from utils.py into _datashader.py
(_ax_show_and_transform, _compute_datashader_canvas_params,
_get_extent_and_range_for_datashader_canvas, _datashader_canvas_from_dataframe,
_create_image_from_datashader_result, _DS_REDUCTION_FUNCS,
_datashader_aggregate_with_function, _datashader_get_how_kw_for_spread,
_prepare_transformation, _apply_cmap_alpha_to_datashader_result,
_datashader_map_aggregate_to_color, _hex_no_alpha, _convert_alpha_to_datashader_range),
removing the utils back-import that was the repo's only near-cycle. _datashader.py
now imports only downward from utils (_fast_extent, _make_continuous_mappable, to_hex).
Also fixes the _datshader_get_how_kw_for_spread -> _datashader_get_how_kw_for_spread
typo (def + caller). Repoints render.py (5 names) and test imports
(test_utils, test_render_points, test_render_shapes) to _datashader.
No behavior change (verbatim move; all-private symbols). Verified: no import cycle;
195 non-visual tests pass; ruff/format clean on moved code. Pre-existing ruff D205 /
mypy Any-return debt in utils.py (#703/#705) is unrelated and fixed by #714;
--no-verify used for that reason only.
Move 21 color helpers + 6 color-only format/uniqueness helpers verbatim from
utils.py into a new sibling module pl/_color.py. Imports flow one way:
_color -> utils (downward, for _get_list/to_hex/_build_alignment_dtype_hint/
_MPL_SINGLE_LETTER_COLORS). render.py, basic.py and _datashader.py are repointed
to import color symbols from _color.
The two validators still in utils (_type_check_params, _validate_graph_render_params)
use color symbols; they carry temporary function-local imports of _color (cycle-safe)
until they move to _validate.py in the next commit, where these become top-level imports.
No behavior change (verbatim move; all-private except set_zero_in_cmap_to_transparent,
which is not re-exported). Verified: no import cycle; 410 non-visual tests pass;
ruff + ruff-format clean. Pre-existing #703/#705 mypy/ruff debt in utils.py is
unrelated; --no-verify for that reason only.
Move 17 validation/type-check functions verbatim from utils.py into a new sibling
module pl/_validate.py. Imports flow one way: _validate -> _color (_is_color_like,
_prepare_cmap_norm, _get_colors_for_categorical_obs, now top-level) and
_validate -> utils (downward). The temporary function-local _color imports added in
the previous commit are hoisted to top-level here and removed from utils.
render.py and basic.py repointed to _validate.
Completes the utils.py split (#696): utils 4918 -> 1311 lines, with
_geometry / _datashader / _color / _validate as single-concern siblings.
No import cycle anywhere. No behavior change (verbatim moves; all-private).
Verified: 410 non-visual tests pass; ruff + ruff-format clean. Pre-existing
#703/#705 mypy debt in utils.py is unrelated; --no-verify for that reason only.
Integrate #714 (show() decomposition) into the utils.py split. Only utils.py
conflicted:
- import block: dropped the now-unused `_locate_value` import (it moved to
_color.py with the color code that uses it); kept main's `_locate_value`
out of utils.
- `_fast_extent` docstring: took main's #714 version (D205 fix).
basic.py auto-merged: #714's decomposed show()/helpers now import color and
validation symbols from _color/_validate (the split's repoints), not utils.
Bonus: merging #714 brings its fixes for the pre-existing #703/#705 debt
(_resolve_measure_table str-return, _get_extent_fast Any-return, _fast_extent
D205), so the branch is now fully ruff + mypy clean (no --no-verify).
Verified: no import cycle; ruff + ruff-format + mypy all pass; 410 non-visual
tests pass.
@timtreistimtreis changed the title refactor: split utils.py into _geometry / _datashader / _color / _validate (#696)refactor: split utils.py into _geometry / _datashader / _color / _validateJun 14, 2026
@codecov-commenter

codecov-commenter commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.41045% with 555 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.48%. Comparing base (7a1b3a3) to head (11e4d2e).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/_color.py57.43%164 Missing and 68 partials ⚠️
src/spatialdata_plot/pl/_validate.py68.57%122 Missing and 109 partials ⚠️
src/spatialdata_plot/pl/_geometry.py75.00%46 Missing and 19 partials ⚠️
src/spatialdata_plot/pl/_datashader.py82.91%15 Missing and 12 partials ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #715 +/- ##
==========================================
+ Coverage 77.18% 77.48% +0.30% 
==========================================
Files 14 17 +3 Lines 4440 4500 +60 Branches 1014 1014 ==========================================
+ Hits 3427 3487 +60 
Misses 658 658 Partials 355 355 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/basic.py82.69% <100.00%> (+0.08%)⬆️
src/spatialdata_plot/pl/render.py88.40% <100.00%> (+0.03%)⬆️
src/spatialdata_plot/pl/utils.py78.88% <ø> (+9.49%)⬆️
src/spatialdata_plot/pl/_datashader.py87.46% <82.91%> (-3.82%)⬇️
src/spatialdata_plot/pl/_geometry.py75.00% <75.00%> (ø)
src/spatialdata_plot/pl/_validate.py68.57% <68.57%> (ø)
src/spatialdata_plot/pl/_color.py57.43% <57.43%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

)
The element-name formatting and table/shape uniqueness helpers
(_format_element_name/s, _preview_values, _ensure_one_to_one_mapping,
_validate_shape_index_uniqueness, _validate_table_instance_uniqueness) were
parked in _color.py only to break the _validate<->_color cycle. They are
cross-cutting low-level helpers, so utils.py is their natural home — this
removes the "validation helpers living in the color module" wart.
Cycle-safe: they call only each other + third-party (GeoDataFrame, pd,
AnnData, get_table_keys), never _color/_validate, so utils gains no edge to
either. _color imports the two it uses (_ensure_one_to_one_mapping,
_format_element_name) from utils; re-added GeoDataFrame/SpatialElement
imports to utils (dropped earlier when geometry/color moved out).
Verified: no import cycle; ruff + ruff-format + mypy clean; 410 non-visual
tests pass.
The utils split (#696) relocated the public set_zero_in_cmap_to_transparent
into the private _color module, breaking the only public import path:
`from spatialdata_plot.pl.utils import set_zero_in_cmap_to_transparent`.
Move it back to utils.py — a public symbol belongs in a public module, and
a re-export shim would re-introduce the _color<->utils cycle the split
removed (utils is the import-graph leaf). Point the test at the public path.
…lit-utils
# Conflicts:
#	src/spatialdata_plot/pl/render.py
@timtreis
timtreis merged commit bfda2ed into mainJun 14, 2026
7 of 8 checks passed
@timtreis
timtreis deleted the refactor/issue-696-split-utils branch June 14, 2026 21:19
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
timtreis added a commit that referenced this pull request Jun 14, 2026
… + single continuous-norm (#699)
PRs 1+2 of #699, rebased onto the post-#715 (utils split) / post-#720 main.
PR3 (image-composite helper) deferred.
CmapParams gains behavior:
- fresh_norm(): a safe per-element copy (applying a Normalize autoscales vmin/vmax
in place, leaking one element's range into the next). Converts the 3 copy(norm)
sites in render.py.
- cmap_with_alpha() / colormap_with_alpha(): public-API replacement for the private
cmap._lut[:, -1] = alpha poke in the 1-channel image path; byte-identical body
colors, no shared-cmap mutation.
Single continuous-norm feeds pixels and colorbar:
- _resolve_continuous_norm(values, cmap_params) in _color.py: one resolver called by
each pixel-baking site and its matching colorbar site with the same vector, so they
cannot diverge. Folds the duplicated inline norm blocks in _color_vector_to_rgba and
_get_collection_shape (now in _geometry.py; its dead `norm` param dropped) and routes
_map_color_seg + the labels imshow + _append_outline_colorbar + the shapes set_clim
through it.
- Datashader / image / points / graph paths unchanged (already share their norm).
Behavior-preserving for normal renders; the only unified edge is an all-identical-value
outline column. Adds unit tests for the new CmapParams methods and _resolve_continuous_norm
plus a non-visual colorbar-clim integration test.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: split utils.py (4454 lines) into _validate.py / _color.py / _geometry.py and finish _datashader.py extraction

2 participants

@timtreis@codecov-commenter