convert tools / coverage / prefix_trie / linestring docstrings to numpydoc - #143
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #143 +/- ##
=======================================
Coverage 94.90% 94.90%
=======================================
Files 10 10
Lines 1434 1434
=======================================
Hits 1361 1361
Misses 73 73
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
Merging this PR will not alter performance
Comparing Footnotes
|
espg
left a comment
There was a problem hiding this comment.
🤖 from Claude (review)
Fresh-context adversarial self-review of claude/135-numpydoc-phase2 @ 320bd7b. Every claim in the PR body reproduced independently; all four gates re-run. No blocking findings. Seven inline comments, all accuracy or follow-up, none of which need to land before the next phase.
Verified clean
Docstring-only diff. Reproduced both proofs per file, independently of the PR's script — parse origin/main and branch, delete every docstring Expr, ast.unparse, compare; then compare the tokenize.COMMENT token text streams:
tools.py: code_identical=True comments_identical=True (123 -> 123)
coverage.py: code_identical=True comments_identical=True ( 17 -> 17)
prefix_trie.py: code_identical=True comments_identical=True ( 20 -> 20)
linestring.py: code_identical=True comments_identical=True ( 0 -> 0)
No comment text migrated into a docstring; no docstring text migrated into a comment.
No rationale lost. Token-level audit per docstring (every identifier, #NNN issue ref, § cross-link, digit and backslash in the main docstring, checked for presence on the branch). The complete set of main tokens absent on the branch across all four files is four words, all of them deliberate summary rewrites:
| function | dropped | why |
|---|---|---|
tools.geo2mort |
Calculates |
→ Compute (D401) |
coverage.moc_not |
of |
"Complement of a morton cover" → "Complement a morton cover" |
coverage.split_base_cells |
each, s |
"keyed by each group**'s**" → "Each group is keyed by its own" |
prefix_trie.MortonChild._compact |
branching |
→ "Branch on divergence" |
Every issue reference survives (#11, #22, #34, #58, #60, #68, #80, #116, #119, #136), as do the spec § cross-links. Read the four flagged sites word by word — _whole_sphere, moc_not, split_base_cells, morton_coverage_moc — and every displaced clause is intact in the Extended Summary.
The morton_coverage_moc relocation was a real bug, not cosmetics. Parsing both versions with griffe's numpy parser (the one mkdocstrings uses) shows what the misplaced paragraph actually did:
MAIN returns: 'numpy.ndarray' -> 'Sorted 1-D array of mixed-order...'
returns: 'For **multipart / holes** (lists of rings), all rings...' -> ''
returns: 'even-odd descent — disjoint parts union with no...' -> ''
returns: 'rings carve holes (a donut is ``[outer, hole]``).' -> ''
BRANCH returns: 'numpy.ndarray' -> 'Sorted 1-D array of mixed-order...'
raises: ValueError
Each line of prose became its own bogus return entry, typed as itself. Fixed here.
The r""" conversions are text-neutral. Compared ast.get_docstring output between main and the branch: moc_or, moc_and, moc_minus, moc_xor are byte-identical. moc_not differs only in the summary/extended-summary rewrite. Readers still see a \ b, confirmed in the rendered site/api/coverage/index.html.
numpydoc structure. Scripted scan of all 40+ docstrings in the four files: zero cases of narrative appearing after a Parameters/Returns/Raises/Warns/See Also header, zero section-ordering violations, Examples last everywhere. Parameter names match signatures throughout. All four modules are rendered by mkdocstrings (docs/api/{tools,coverage,prefix_trie,linestring}.md), so the strict build is a meaningful gate rather than a no-op.
Examples run, and resolve for a reader. mortie.mort2healpix(m) resolves after the >>> import mortie shown in the example — confirmed. The bare mort2healpix(m) form on main only passed because doctest.testmod injects the defining module's globals, so the fix is real.
Gates, re-measured on this branch
| gate | result |
|---|---|
pytest -q |
804 passed, 12 skipped, 10.69s — matches the claim and the main baseline |
flake8 mortie --select=E9,F63,F7,F82 |
exit 0 |
ruff check --select D (four files) |
All checks passed!, exit 0 |
mkdocs build --strict |
exit 0 (read directly: > /dev/null 2>&1; echo $?), no warnings beyond the Material 2.0 banner |
doctest.testmod |
tools 11/0, coverage 18/0, prefix_trie 0/0, linestring 8/0 — 37 examples, 0 failures |
same, on main's versions |
linestring 1 failure, fixed here |
ruff check (full, four files) |
exit 1 — the single pre-existing F841 |
The F841 in "Questions for review" (1) checks out as pre-existing: on_antimeridian is assigned and never read at mortie/tools.py line 945 on main (line 975 here), inside _normalize_antimeridian_polygon. Untouched here is the right call for a docstring-only diff; agreed it wants its own issue.
Findings
Medium — 1
prefix_trie.geo_morton_polygon/morton_polygon_from_array: the newReturnstext "Refined prefix-cells (len <= n_cells)" states a guarantee the code does not make.morton_polygon_from_array(m, n_cells=1)returns 6 nodes when the trie has 6 roots. Pre-existing wording onmorton_polygon, newly copied onto two more public functions. Inline, with repro.
Low — 4 (all inline)
morton_coverage_moc's newRaisesomits the multipart ring-countValueErrorfrom_prep_rings, now thatParametersadvertises the multipart form.mort2healpixstates the same-order precondition inNotesbut gained noRaises, whilemort2normandgenerate_morton_childrendid._expansion_efficiency: "undefined for a leaf" is actually a negative float, and the single-child case it warns about is unreachable.MortonChild's newRaises: the leading-character clause only fires whenstart_col > 0.
Suggestions — 2 (inline, explicitly out of scope for this PR)
- The newly pinned
[(10,), (27,)]inlinestring_coverageis a behavioural assertion no CI job runs. - Nothing enforces that
docs/morton_index_datatype.mdkeeps quoting the realImportError— the drift this commit exists to fix.
Observations, no action asked
- Eight blank lines were deleted after docstrings in
coverage.py(compress_moc,moc_to_order, the four set ops,common_ancestor,split_base_cells) — ruffD202. These are the only lines in the four-file diff that are not docstring content. Whitespace-only, invisible toast.unparse, so the docstring-only claim stands; noting it only because "docstring-only" is stated as an absolute. :func:/:attr:roles render literally on the site. mkdocstrings in Markdown mode does not process Sphinx roles, so:func:moc_or`` publishes as the literal text:func: moc_or— visible in the built `site/`. Thoroughly pre-existing (the `coverage` module docstring on `main` does it, unchanged here) and pervasive, and this PR adding more is consistent with the surrounding code per the repo conventions. Flagging so it is on the record as a repo-wide docs decision, not as something to change in this PR.- On "Questions for review" (3): the
MortonChildAttributessection reads well and renders as a table, but it lists four of the six names the prose above calls the frozen read surface —mantissa_arrayandcell_areaare omitted because they are properties with their own entries. That is defensible numpydoc, just worth being a deliberate choice rather than an accident. - On "Questions for review" (2):
tools.pymeasures 1348 lines here. Flagging rather than splitting is the right read of the conventions on a docs-only change.
Generated by Claude Code
| Returns | ||
| ------- | ||
| list of MortonChild | ||
| Refined prefix-cells (len <= *n_cells*). |
There was a problem hiding this comment.
🤖 from Claude (review)
len <= *n_cells* is not a guarantee the code makes. morton_polygon starts from current = list(roots) and only ever grows that list, so the budget is honoured only once it is already at least the root fan-out. Reproduced on this branch:
>>> lats = np.array([-80., -70., 10., 20., 60., 65., 30., -30.])
>>> lons = np.array([0., 45., 90., 135., 180., -135., -90., -45.])
>>> m = mortie.geo2mort(lats, lons, order=6)
>>> len(split_children(m, max_depth=4))
6
>>> [len(morton_polygon_from_array(m, n_cells=n)) for n in (1, 2, 3, 4)]
[6, 6, 6, 6]The wording is copied from morton_polygon's own Returns, which is unchanged from main, so the inaccuracy is pre-existing — but this PR newly propagates it onto two more public functions (geo_morton_polygon here, morton_polygon_from_array at line 397), and the n_cells parameter line right above (line 363, "Maximum number of cells in the returned list") overstates it the same way.
Suggested wording for the three Returns blocks:
Refined prefix-cells — at most n_cells, but never fewer than the number of root-level groups, which sets the floor when the fan-out already exceeds the budget.
plus the matching caveat on the n_cells parameter. Since the two new copies are yours, fixing all three together keeps them from drifting apart.
Generated by Claude Code
| Returns | ||
| ------- | ||
| list of MortonChild | ||
| Refined prefix-cells (len <= *n_cells*). |
There was a problem hiding this comment.
🤖 from Claude (review)
Second copy of the len <= *n_cells* overstatement — see the comment on geo_morton_polygon (line 373) for the repro. Same fix applies here and to n_cells at line 389.
Generated by Claude Code
| ValueError | ||
| If ``order`` lies outside 1-29, both ``tolerance`` and ``max_cells`` | ||
| are given, the ring's lats and lons have different lengths, there are | ||
| fewer than 3 vertices, or a coordinate is NaN/infinity. |
There was a problem hiding this comment.
🤖 from Claude (review)
This new Raises under-claims the multipart path. The Parameters block directly above now advertises "or a list of such arrays for the multipart form", and that path routes through _prep_rings, which raises a fourth ValueError this list does not cover:
if len(lats) != len(lons):
raise ValueError("lats and lons must have the same number of rings")"the ring's lats and lons have different lengths" is the within-ring mismatch (_prep_rings, line 104). The ring-count mismatch (line 98) is a distinct failure, and you already worded it correctly on _prep_rings itself (line 93: "If the ring counts differ"). Suggest folding the same clause in here so the two agree.
Generated by Claude Code
|
|
||
| Notes | ||
| ----- | ||
| The function converts morton indices to HEALPix NESTED scheme cell IDs. |
There was a problem hiding this comment.
🤖 from Claude (review)
mort2healpix has no Raises, but it does raise. The Notes you just relocated here state the precondition — "All input morton indices must be at the same order" — and it is enforced, one frame down: mort2healpix calls mort2norm, which raises
ValueError: Mixed orders in morton array: [...]; use orders_of for per-element orders
This PR added a Raises section to mort2norm (line 606) and to generate_morton_children (line 1154) for exactly this class of contract, so mort2healpix reads as the one that got skipped — and it is the function whose docstring states the constraint without saying what happens when it is violated. Suggest:
Raises
------
ValueError
If the words are at mixed orders (propagated from :func:`mort2norm`).
placed before Notes.
Generated by Claude Code
| ---------- | ||
| node : MortonChild | ||
| An expandable node (``nchildren > 1``; ``cost`` is zero for a single | ||
| child and undefined for a leaf). |
There was a problem hiding this comment.
🤖 from Claude (review)
Two small inaccuracies in this new parameter note.
(1) "undefined for a leaf" — for a leaf nchildren == 0, so cost == -1 and benefit == node.cell_area; benefit / cost returns a negative float. Nothing is undefined and nothing raises, it just silently returns a meaningless negative score. "negative and meaningless for a leaf" would be exact.
(2) "cost is zero for a single child" describes a state the trie cannot reach. _compact only creates children in the divergence branch (line 178), which is guarded by len(unique) == 1 being false — so nchildren is always 0 or >= 2, never 1. As written the note advertises a ZeroDivisionError no caller can trigger.
Neither is load-bearing; flagging because the sentence is new text and a reader will take it at face value.
Generated by Claude Code
| ValueError | ||
| If *mask* selects no rows, or the masked rows do not share the | ||
| expected leading character (an uncompressible mix of signs or base | ||
| cells). |
There was a problem hiding this comment.
🤖 from Claude (review)
The second clause of this new Raises is conditional, and the condition is not stated. __init__ only checks the shared leading character when start_col > 0 (line 145):
if start_col > 0:
if len(np.unique(char_array[mask, 0])) > 1:
raise ValueError(
"Input array is not compressible — "
"indices do not share expected prefix"
)A root-level construction (start_col == 0) skips it entirely, so as written this over-claims for the root case. "…or, when start_col is nonzero, the masked rows do not share the expected leading character" matches the code.
The first clause ("If mask selects no rows") is unconditional and correct — that is line 139.
Generated by Claude Code
| >>> lons_parts = [[-120.0, -120.0], [-80.0, -70.0, -60.0]] | ||
| >>> per_line = mortie.linestring_coverage(lats_parts, lons_parts, order=6) | ||
| >>> [arr.shape for arr in per_line] | ||
| [(10,), (27,)] |
There was a problem hiding this comment.
🤖 from Claude (review)
Confirmed independently: this example really does fail on main (Failed example: [arr.shape for arr in per_line] / Expected nothing / Got: [(10,), (27,)]), and doctest.testmod on the branch version is TestResults(failed=0, attempted=8). Good fix.
The follow-on worth naming: [(10,), (27,)] is now a pinned behavioural assertion that nothing in CI runs. pyproject.toml's [tool.pytest.ini_options].addopts has no --doctest-modules, testpaths is ["mortie/tests"], and no workflow invokes doctest. If the Rust linestring sampler ever changes its half-cell arc spacing, this becomes wrong documentation silently — the exact failure mode the fix just cleaned up, one layer along.
Two ways out, either acceptable:
(1) add --doctest-modules scoped to the non-test modules, which turns the 37 examples across the four converted modules into a real gate;
(2) leave it, and fold doctest enforcement into the still-open "numpydoc validation in CI" question from the originating issue, so it is a recorded decision rather than an oversight.
Out of scope for a docstring-only PR either way — not asking for it here.
Generated by Claude Code
| assert "`pip install pandas`" in message | ||
| assert "`pip install mortie[pandas]`" in message | ||
| assert "pins pandas as a mortie dependency" in message | ||
| assert "installed alongside mortie" in message |
There was a problem hiding this comment.
🤖 from Claude (review)
Verified the ImportError commit end to end and it holds:
- exactly one string literal for this message in the tree (
morton_index.pyline 289;ast.walkover everyraise ImportError(...)in that module returns a single literal); mortie/pandas.pyreaches it via_require_pandas()(line 40), no second copy;- all three raise paths emit the identical string —
test_message_is_identical_on_every_pathassertslen(set(msgs)) == 1and passes; docs/morton_index_datatype.mdmatches character for character — I concatenated the four wrapped comment lines, stripped the#prefixes and the enclosing quotes, and compared toast.literal_evalof the literal: exact match, 244 chars;- no stale copy of the old wording anywhere tracked by git (
pins pandas,loaded whenever mortie,Install it directlyall return zero hits outside.venv/).
The gap: nothing enforces the docs quote. These assertions are substring matches, and the markdown is not checked at all — which is precisely how the previous wording was able to sit in the docs asserting something the code did not mean. A guard that needs no production-code change, reusing the existing helper:
def test_docs_quote_matches_the_message(self):
out = _run_without_pandas(
"import mortie\n"
"try:\n mortie.MortonIndexArray\n"
"except ImportError as exc:\n print(exc)\n"
)
doc = (Path(__file__).parents[2] / "docs" / "morton_index_datatype.md").read_text()
block = re.search(
r'# "the morton_index ExtensionArray requires pandas.*?"\n', doc, re.S
).group(0)
quoted = " ".join(
re.sub(r"^#\s?", "", ln).strip() for ln in block.strip().splitlines()
).strip('"')
assert quoted == out.stdout.strip()Non-blocking, and arguably its own small change rather than this PR's — but it is cheap and it closes the loop the commit was opened to fix.
Generated by Claude Code
|
🤖 from Claude Adversarial self-review ran against the four converted modules and the It also confirmed the Five diff-scoped findings are addressed in (1) A current = list(roots)
count = len(current)
...
while count < n_cells and heap:so when the trie has more root-level children than the budget, the budget is ignored. Reproduced on a 7-root trie — Flagging this as substantively wrong prose, not merely unstructured: the claim predates this PR on (2) (3) (4) (5) Two review suggestions left standing as out of scope, both needing a decision rather than a fix:
Re-verified after the fixes: Generated by Claude Code |
yes, own issue. this too:
yes, and it's already tracked as a seperate issue in #140 |
yeah, that behavior needs to be fixed. We shouldn't ignore... raising, warning, or pinning to a new threshold would all be better options... |
seems minor and possibly worth doing... |
Closes #135. Refs #68.
Phase 2 of the repo-wide numpydoc sweep: the four modules deferred from phase 1 (PR #141) to avoid colliding with PR #130 and PR #139. Both of those have merged, so the contention is gone. CLAUDE.md §7 now mandates numpydoc, so this brings the last of
mortie/in line with it and with mkdocstrings'docstring_style: numpy.Every public function, class and method in the four modules now carries a one-line imperative summary, then
Parameters/Returns/Raises/Warnswhere applicable, with the existing design rationale preserved verbatim in the Extended Summary rather than compressed into parameter tables.Phases
mortie/tools.py—convert tools docstrings to numpydoc (issue #135)mortie/coverage.py—convert coverage docstrings to numpydoc (issue #135)mortie/prefix_trie.py—convert prefix_trie docstrings to numpydoc (issue #135)mortie/linestring.py—convert linestring docstrings to numpydoc (issue #135)ImportErrorwording correction (own commit, see below)address self-review findings (issue #135))Per-module before/after
Two metrics, read together — neither alone is the finish line.
ruffnever requires aParameterssection to exist, so a file can be ruff-green with no numpydoc structure at all (that waslinestring.py's starting state). Converselytools.pyandcoverage.pywere largely sectioned but carried the highestDcounts: formatting defects inside already-structured docstrings.Parametersblocks before → afterDbefore → aftermortie/tools.pymortie/coverage.pymortie/prefix_trie.pymortie/linestring.pyThe counts that stop short of the def count are correct, not gaps:
coverage.py15/16 —_whole_sphere()takes no arguments, so it hasReturnsand noParameters.prefix_trie.py11/15 — the four without aParametersblock areMortonChild.__init__(constructor arguments are documented on the class, per numpydoc), themantissa_arrayandcell_areaproperties, and__repr__. None takes a documentable argument; all now have summaries, andReturnswhere they return something.What changed beyond adding sections
coverage.py— narrative parsed as parameters.morton_coverage_mochad its multipart/holes paragraph sitting after theReturnsheader, which numpydoc reads as further return entries. Moved into the Extended Summary, aboveParameters. This was a live rendering bug, not a style nit: the self-review confirmed griffe's numpy parser emits fourreturnsentries for that function onmain— one real, three bogus, one per line of the stranded paragraph. On the branch it emits onereturnsplus oneraises. The adaptive-stop-criteria lead-in also dangled on a colon directly before theParametersheader; it now namestoleranceandmax_cellsand ends in a period. No words dropped.coverage.py— five docstrings converted tor"""(moc_or,moc_and,moc_minus,moc_xor,moc_not) so thea \ bset-difference notation stops needing\\(ruffD301). The runtime docstring text is byte-identical tomainfor four of them, and differs only in the summary rewrite formoc_not— the escape change is purely how the literal is spelled, not what it says.coverage.py— two two-line summaries collapsed (moc_not,split_base_cells, ruffD205); the displaced clause moved to the first line of the Extended Summary, intact.tools.py—geo2mort's summary wasCalculates ..., with no period and with the closing quotes on the content line (D400/D401/D209); it is nowCompute morton indices from geographic coordinates.Three more summaries gained their missing period (mort2norm,mort2geo,mort2bbox).tools.py— section order fixed inres2displayandmort2healpix, which hadExamplesbeforeSee Also/Notes. numpydoc rendersExampleslast.linestring.py— the multi-linestring example did not run.>>> [arr.shape for arr in per_line]had no expected output, so it failed underdoctest. It now carries the real output,[(10,), (27,)], and both examples moved out of::literal blocks into plain doctest blocks matchingmorton_coverage's style.coverage._single_coverage'snormalizedefers tomorton_coverageby name for the full ring-winding contract rather than restating an abridged version — the failure mode PR Convert morton_index, geometry and arrow docstrings to numpydoc #141's review caught onfrom_wkb/from_wkt, where an apparently-complete table quietly dropped a caveat.Substantively wrong prose found
Flagged separately from the formatting work, because this is a docstring that says something false rather than one that is merely unstructured.
morton_polygon'sReturnsonmainreadsRefined prefix-cells (len <= *n_cells*)., and itsn_cellsparameter readsMaximum number of cells in the returned list.Neither holds.morton_polygonseeds the frontier with the roots and only ever grows it:so when the trie has more root-level children than the budget, the budget is simply ignored. Reproduced on a 7-root trie:
morton_polygon_from_array(m, n_cells=n)returns 7 cells for everynin 1..7. Roots are expanded, never merged, solen(roots)is a floor on the result.I hit this because I had propagated the same sentence onto
geo_morton_polygonandmorton_polygon_from_arraywhile filling in their missing return descriptions; the self-review caught it there. All three now describe the root-count floor, and then_cellslines no longer claim a maximum.The behavioural question is open and left for review (see "Questions for review" (1)): whether the floor is intended, or
morton_polygonshould rejectn_cells < len(roots)rather than silently overshoot. No code changed either way.Missing-pandas
ImportErrorwordingSeparate commit,
correct the missing-pandas ImportError wording (issue #135), so it can be reviewed or reverted independently of the conversion.The message introduced in PR #141 was factually wrong, not merely unclear. Its final clause:
made two false claims. (1) "pins" — the extra is
pandas>=2.0inpyproject.toml, a lower bound, not a pin. (2) "so it is loaded whenever mortie is imported" — equally true after a plainpip install pandas; the eager probe at the bottom ofmorton_index.pykeys off whether pandas is importable, not how it was installed. The clause therefore invented a difference between the two remedies it offered, and could read as implying the bare install leaves the ExtensionArray unavailable. Replaced with:which states the real difference — the extra records the dependency in mortie's metadata, so a fresh install, a lockfile, or another machine picks pandas up automatically.
Updated in all three places that carry it, and re-grepped afterwards to confirm nothing else quotes it (the only remaining hits are in the gitignored
htmlcov/coverage artifacts):mortie/morton_index.py— the single string literal. That property is preserved:mortie/pandas.pystill reaches it through_require_pandas()rather than duplicating it.mortie/tests/test_pandas_module.py— the three assertions matching"pins pandas as a mortie dependency"re-pointed at"installed alongside mortie". Assertions kept, not deleted, and still substring matches rather than whole-paragraph matches, which would be brittle.docs/morton_index_datatype.md— the verbatim quote updated to match exactly (verified character for character, 244 chars).test_message_is_identical_on_every_pathalready asserts that all three raise paths (mortie.MortonIndexArray,from mortie.morton_index import ...,from mortie.pandas import ...) produce the same string (len(set(msgs)) == 1); it passes.How it was tested
The diff on the four converted modules is docstring-only, proved two ways per file (the AST check alone is blind to comments — PR #141 found six comment blocks that had migrated into docstrings):
origin/mainversion and the branch version, delete every docstringExprnode,ast.unparseboth, compare.tokenize.COMMENTtoken stream (text only, position ignored) from both and compare.The only non-docstring lines in the four-file diff are eight
D202blank-line deletions incoverage.py(a blank line between a docstring and the first statement).The
ImportErrorcommit is deliberately not docstring-only — it changes a string literal, its assertions and its doc quote — which is why it is a separate commit.Other gates, all run in a clean venv on this branch, and all re-run after the self-review fixes:
pytest— 804 passed, 12 skipped, exactly the baseline measured onorigin/main@3f306a8before any edit (804 passed, 12 skipped). No movement, so no logic was touched.ruff check --select Don each of the four files — clean, exit 0.flake8 mortie --select=E9,F63,F7,F82— clean, exit 0.mkdocs build --strict— exit 0, zero warnings. Exit code read directly from the build, not through a pipe.doctest.testmodon all four modules — 37 examples, 0 failures. Onmainlinestring.pyhad 1 failure; it is fixed here.An independent fresh-context adversarial review re-ran every one of those and reproduced the same numbers, and found nothing on lost rationale (a token-level audit found four
mainwords absent, all deliberate summary rewrites; every issue ref and§link survives), lost or moved comments, the docstring-only claim, non-running examples,r"""text neutrality, or section ordering. Its five diff-scoped findings are folded into389ce57and itemised in the review-response comment.Questions for review
(1) Is
morton_polygon's root-count floor intended? Per the "Substantively wrong prose" section above,n_cellsis not honoured when the trie has more roots than the budget — you getlen(roots)cells back with no error. The docstrings now describe that faithfully, but the two plausible intents point different ways: (a) the floor is inherent (you cannot describe the data in fewer cells than it has root groups) and the docs are now correct as written; (b) silently returning more cells than asked for is a bug andmorton_polygonshould raise whenn_cells < len(roots). I did not change behaviour. If (b), it wants its own issue rather than a docs-only PR.(2)
_normalize_antimeridian_polygonhas an unused variable.ruff check mortie/tools.pyreportsF841foron_antimeridianat line 975 — assigned fromnp.sum(...)and never read. It is pre-existing onmain(line 945 there) and fixing it is a logic change, so it is deliberately untouched here to keep the diff docstring-only. The neighbouringmort2bboxcomputes the same quantity as a mask and uses it; this one computes a count and drops it, which looks like the vestige of an earlier version of the check. Worth its own issue?(3)
tools.pyis 1354 lines, over CLAUDE.md §4's ~1000-line guidance. It was already 1315 onmain; this PR adds 39 lines of docstring and no code. Flagging rather than splitting, since §4 asks for discussion first and a split here would be churn on top of a docs-only change.(4)
MortonChildgained anAttributessection listing the frozen 1.x read surface (characteristic,len,children,nchildren). The prose above it already enumerated those names; the section makes mkdocstrings render them as a table. Say the word if you would rather the prose stayed the single source and the section came out.(5)
mort2healpix's example now callsmortie.mort2healpix(m)rather than the baremort2healpix(m). The bare form only worked becausedoctest.testmodruns in the defining module's globals; a reader copying the rendered example afterimport mortiewould have hit aNameError. The example still runs and still printsHEALPix cell 37010 at order 6.(6) Doctest examples are not executed by CI. The
[(10,), (27,)]output now pinned inlinestring_coverageis a behavioural assertion nothing runs — there is no--doctest-modulesinaddoptsand no workflow invokes doctest. I verified all 37 examples by hand for this PR, but the next docstring change will not be checked. Adding--doctest-modulesis apyproject.tomlchange, which this PR is scoped out of; worth a separate issue?(7) Enforcement is still open from #135 itself — whether to add a
numpydocvalidation pass tolint.yml, or leave theruffDrules as the mechanical floor with numpydoc structure as convention. All ofmortie/is converted now, so it is decidable; it is out of scope for this PR.