From fa28397a288ebe53ba2489bf5fa15b95feb830be Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Sun, 23 Aug 2026 18:35:44 +0000 Subject: [PATCH 1/2] docs: document the reexported BandedMatrices API Per the requirement that every reexport restoration also document the reexported surface in the package's own docs, expand the "Reexported from BandedMatrices.jl" section of `docs/src/api.md` (already wired into `docs/make.jl`) and mirror it in the README: * names grouped by role -- building the bands, populating them, querying them, errors -- rather than a flat list; * the owning package named and linked to its docs (https://juliaLinearAlgebra.github.io/BandedMatrices.jl/stable/), stating plainly that FastAlmostBandedMatrices only re-exports these names and neither documents nor defines them; * an explicit boundary line: anything else from BandedMatrices.jl must be imported from BandedMatrices.jl directly; * the deliberate exclusions with their reasons -- the FillArrays.jl names `Fill`, `Ones`, `Zeros` and `Eye` (a whole dependency chain of BandedMatrices, none of it needed to build an `AlmostBandedMatrix`), the `BandedMatrices` module binding itself, and `symrcm`; * a note that `Band` and `BandError` carry no upstream docstring, which is why they sit in the `api_docs_kwargs` ignore list in `test/qa/qa.jl`, and that the docstring is owed by BandedMatrices.jl. The list now lives in three places -- the `export` block in `src/FastAlmostBandedMatrices.jl`, `REEXPORTED_API` in `test/qa/qa.jl`, and the docs section -- so the Core testset now parses the latter two and asserts all three are the same set, rather than only checking the exports. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Rmh6B9eoW3N8oCbuuVPVxJ --- README.md | 32 +++++++++++++++++----------- docs/src/api.md | 52 ++++++++++++++++++++++++++++++++-------------- test/core_tests.jl | 27 ++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 357cdd7..a898c22 100644 --- a/README.md +++ b/README.md @@ -191,18 +191,26 @@ almostbandedrank finish_part_setindex! ``` -Because the documented constructor is `AlmostBandedMatrix(bands::BandedMatrix, fill)`, -`using FastAlmostBandedMatrices` also brings along the -[BandedMatrices.jl](https://github.com/JuliaLinearAlgebra/BandedMatrices.jl) names needed to -build, populate and query that `bands` argument. They remain owned and documented by -BandedMatrices.jl: - -``` -BandedMatrix brand brandn -Band BandRange band bandrange -bandwidth bandwidths colrange rowrange -BandError -``` +### Reexported from BandedMatrices.jl + +`AlmostBandedMatrix(bands::BandedMatrix, fill)` is the documented constructor and the +examples above build `bands` with `brand`, so `using FastAlmostBandedMatrices` also brings +in the [BandedMatrices.jl](https://juliaLinearAlgebra.github.io/BandedMatrices.jl/stable/) +names needed to build that banded argument, populate it and query it: + + - Building the bands: `BandedMatrix`, `brand`, `brandn` + - Populating them: `Band`, `BandRange`, `band`, `bandrange` + - Querying them: `bandwidth`, `bandwidths`, `colrange`, `rowrange` + - Errors: `BandError` + +These names are owned and documented by +[BandedMatrices.jl](https://juliaLinearAlgebra.github.io/BandedMatrices.jl/stable/); +FastAlmostBandedMatrices.jl only re-exports them and does not document them. Anything else +from BandedMatrices.jl must be imported from BandedMatrices.jl directly. In particular, +the FillArrays.jl names it reexports in turn (`Fill`, `Ones`, `Zeros`, `Eye`), the +`BandedMatrices` module binding itself, and `symrcm` are deliberately not passed along. +See the [API page](https://docs.sciml.ai/FastAlmostBandedMatrices/stable/api/) for the +details. ## Some Considerations diff --git a/docs/src/api.md b/docs/src/api.md index 93eb415..8538891 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -31,20 +31,40 @@ finish_part_setindex! ## Reexported from BandedMatrices.jl -The documented constructor is `AlmostBandedMatrix(bands::BandedMatrix, fill)`, and the +`AlmostBandedMatrix(bands::BandedMatrix, fill)` is the documented constructor, and the examples on the home page build `bands` with `brand`. So `using FastAlmostBandedMatrices` -also brings the -[BandedMatrices.jl](https://github.com/JuliaLinearAlgebra/BandedMatrices.jl) names needed to -construct, populate and query that banded argument: - -| Purpose | Names | -|:--------|:------| -| Construction | `BandedMatrix`, `brand`, `brandn` | -| Band indexing | `Band`, `BandRange`, `band`, `bandrange` | -| Structure queries | `bandwidth`, `bandwidths`, `colrange`, `rowrange` | -| Errors | `BandError` | - -These names are owned and documented by BandedMatrices.jl; see its documentation for their -full behaviour. Nothing else from BandedMatrices.jl is reexported, and neither are the -names it reexports from its own dependencies (for example the FillArrays.jl names `Fill`, -`Ones`, `Zeros` and `Eye`). Use `using BandedMatrices` or `using FillArrays` for those. +also brings in the [BandedMatrices.jl](https://juliaLinearAlgebra.github.io/BandedMatrices.jl/stable/) +names needed to build that banded argument, populate it and query it, so they do not have +to be imported separately: + + - Building the bands: `BandedMatrix`, `brand`, `brandn` + - Populating them: `Band`, `BandRange`, `band`, `bandrange` + - Querying them: `bandwidth`, `bandwidths`, `colrange`, `rowrange` + - Errors: `BandError` + +These names are owned and documented by +[BandedMatrices.jl](https://juliaLinearAlgebra.github.io/BandedMatrices.jl/stable/). +FastAlmostBandedMatrices.jl only re-exports them; it does not document them and does not +define their behaviour, so BandedMatrices.jl's own documentation is the reference for what +each one does. + +Anything else from BandedMatrices.jl must be imported from BandedMatrices.jl directly. +In particular, the following are deliberately **not** reexported: + + - The [FillArrays.jl](https://juliaarrays.github.io/FillArrays.jl/stable/) names + `Fill`, `Ones`, `Zeros` and `Eye`. BandedMatrices.jl reexports those from a + dependency of its own, so passing them along here would be reexporting a whole + dependency chain; none of them is needed to build an `AlmostBandedMatrix`. Use + `using FillArrays`. + - The `BandedMatrices` module binding itself. Use `import BandedMatrices` if you want + the qualified form. + - `symrcm`, a sparse-matrix reordering unrelated to this package. + +`Band` and `BandError` have no docstring upstream in BandedMatrices.jl, so they are listed +in the `api_docs_kwargs` ignore list in `test/qa/qa.jl` rather than documented here; that +docstring is owed by BandedMatrices.jl. Every other reexported name above carries its own +upstream docstring, reachable from the REPL help mode. + +The list above is kept in sync with the reexport `export` block in +`src/FastAlmostBandedMatrices.jl` and with `REEXPORTED_API` in `test/qa/qa.jl`, and the +Core test suite asserts that all three agree. diff --git a/test/core_tests.jl b/test/core_tests.jl index e990663..0804220 100644 --- a/test/core_tests.jl +++ b/test/core_tests.jl @@ -4,8 +4,8 @@ using SafeTestsets using FastAlmostBandedMatrices import BandedMatrices - # Kept in sync with the reexport `export` block in src/FastAlmostBandedMatrices.jl and - # with `REEXPORTED_API` in test/qa/qa.jl. + # Kept in sync with the reexport `export` block in src/FastAlmostBandedMatrices.jl, + # `REEXPORTED_API` in test/qa/qa.jl, and the reexport section of docs/src/api.md. reexports = ( :Band, :BandError, :BandRange, :BandedMatrix, :band, :bandrange, :bandwidth, :bandwidths, :brand, :brandn, :colrange, :rowrange, @@ -26,6 +26,29 @@ using SafeTestsets @test name ∉ exported end + # The same list lives in three places: the `export` block in src (checked above via + # `names`), `REEXPORTED_API` in test/qa/qa.jl, and the "Reexported from + # BandedMatrices.jl" section of docs/src/api.md. They must not drift apart. + qa_file = joinpath(@__DIR__, "qa", "qa.jl") + if isfile(qa_file) + block = match(r"REEXPORTED_API = \((.*?)\)"s, read(qa_file, String)) + @test block !== nothing + declared = Set(Symbol(m[1]) for m in eachmatch(r":(\w+)", block[1])) + @test declared == Set(reexports) + end + + docs_file = joinpath(@__DIR__, "..", "docs", "src", "api.md") + if isfile(docs_file) + section = match( + r"## Reexported from BandedMatrices\.jl(.*?)\n\nThese names are owned"s, + read(docs_file, String), + ) + @test section !== nothing + bullets = join(filter(startswith(" - "), split(section[1], '\n')), '\n') + documented = Set(Symbol(m[1]) for m in eachmatch(r"`(\w+)`", bullets)) + @test documented == Set(reexports) + end + # The documented construction path works with `using FastAlmostBandedMatrices` alone. A = AlmostBandedMatrix(brand(Float64, 10, 10, 3, 2), rand(Float64, 2, 10)) B = AlmostBandedMatrix(BandedMatrix(fill(1.0, 10, 10), (3, 2)), rand(Float64, 2, 10)) From 3f552b7a9c100c913c3b6de151fedcee5e02dd3f Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Sun, 23 Aug 2026 19:27:48 +0000 Subject: [PATCH 2/2] test: make the reexport sync check line-ending agnostic The docs/export sync assertions added in the previous commit failed on all three Windows lanes while Linux and macOS passed. Git checks the repository out with CRLF line endings on Windows, and the section was located with a regex anchored on `\n\nThese names are owned`, which cannot match `\r\n\r\n`. `match` returned `nothing`, so `@test section !== nothing` failed and the following line then errored dereferencing `nothing`. Verified directly: the old regex run against a CRLF copy of docs/src/api.md returns `nothing`. Fixes: * Both files are now read through a helper that normalises `\r\n` and bare `\r` to `\n` before anything is matched, so no pattern depends on the checkout's line endings. * The docs section is located structurally rather than by prose: from the `## Reexported from BandedMatrices.jl` heading to the next `## ` heading (or end of file), taking the first contiguous run of ` - ` bullets. That run is the reexported names; the second bullet list in the same section is the deliberate exclusions, which must not be picked up. The check no longer depends on the wording of any paragraph, so ordinary prose edits cannot break it. * Every lookup now reports what it was looking for and where before failing: the file, the heading or pattern, and for a genuine drift the symmetric difference between the documented names and the exports. Each result is also guarded so a failed lookup cannot cascade into an error on `nothing`. The parse-the-docs approach is kept rather than downgraded to a plain "docs mention each name" check: containment alone would not catch a name documented in the exclusions list, nor a stale name left behind in the docs after it was dropped from the exports. Checked against both an LF and a synthetic CRLF copy of docs/src/api.md and test/qa/qa.jl (both pass), and against a deliberately drifted CRLF copy with `brandn` removed from the docs bullets, which fails with `only_in_exports = Set([:brandn])`. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Rmh6B9eoW3N8oCbuuVPVxJ --- test/core_tests.jl | 65 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/test/core_tests.jl b/test/core_tests.jl index 0804220..14729fd 100644 --- a/test/core_tests.jl +++ b/test/core_tests.jl @@ -29,24 +29,67 @@ using SafeTestsets # The same list lives in three places: the `export` block in src (checked above via # `names`), `REEXPORTED_API` in test/qa/qa.jl, and the "Reexported from # BandedMatrices.jl" section of docs/src/api.md. They must not drift apart. + # + # Both files are read line-wise after normalising the line endings: git checks these + # files out with CRLF on Windows, so nothing here may assume "\n". + readlines_lf(path) = split(replace(read(path, String), "\r\n" => "\n", "\r" => "\n"), '\n') + qa_file = joinpath(@__DIR__, "qa", "qa.jl") if isfile(qa_file) - block = match(r"REEXPORTED_API = \((.*?)\)"s, read(qa_file, String)) + qa_source = join(readlines_lf(qa_file), '\n') + block = match(r"REEXPORTED_API = \((.*?)\)"s, qa_source) + block === nothing && + @error "reexport sync: no `REEXPORTED_API = (...)` found" file = qa_file @test block !== nothing - declared = Set(Symbol(m[1]) for m in eachmatch(r":(\w+)", block[1])) - @test declared == Set(reexports) + if block !== nothing + declared = Set(Symbol(m[1]) for m in eachmatch(r":(\w+)", block[1])) + declared == Set(reexports) || @error( + "reexport sync: REEXPORTED_API disagrees with the exports", + file = qa_file, only_in_file = setdiff(declared, Set(reexports)), + only_in_exports = setdiff(Set(reexports), declared) + ) + @test declared == Set(reexports) + end end docs_file = joinpath(@__DIR__, "..", "docs", "src", "api.md") if isfile(docs_file) - section = match( - r"## Reexported from BandedMatrices\.jl(.*?)\n\nThese names are owned"s, - read(docs_file, String), - ) - @test section !== nothing - bullets = join(filter(startswith(" - "), split(section[1], '\n')), '\n') - documented = Set(Symbol(m[1]) for m in eachmatch(r"`(\w+)`", bullets)) - @test documented == Set(reexports) + heading = "## Reexported from BandedMatrices.jl" + lines = readlines_lf(docs_file) + # Structural, not prose-anchored: the section runs from its heading to the next + # `## ` heading (or the end of the file). + is_bullet(line) = startswith(line, " - ") + start = findfirst(line -> rstrip(line) == heading, lines) + start === nothing && + @error "reexport sync: heading not found in docs" file = docs_file heading + @test start !== nothing + if start !== nothing + stop = findnext(line -> startswith(line, "## "), lines, start + 1) + section = lines[start:(stop === nothing ? lastindex(lines) : stop - 1)] + # Only the first contiguous run of bullets under the heading is the list of + # reexported names (" - Building the bands: `BandedMatrix`, ..."); the later + # bullet list in the same section is the deliberate *exclusions*. + bullets = String[] + idx = findfirst(is_bullet, section) + while idx !== nothing && idx <= lastindex(section) && is_bullet(section[idx]) + push!(bullets, String(section[idx])) + idx += 1 + end + isempty(bullets) && @error( + "reexport sync: no ` - ` role bullets under the heading", + file = docs_file, heading + ) + @test !isempty(bullets) + documented = Set( + Symbol(m[1]) for line in bullets for m in eachmatch(r"`(\w+)`", line) + ) + documented == Set(reexports) || @error( + "reexport sync: the docs section disagrees with the exports", + file = docs_file, only_in_docs = setdiff(documented, Set(reexports)), + only_in_exports = setdiff(Set(reexports), documented) + ) + @test documented == Set(reexports) + end end # The documented construction path works with `using FastAlmostBandedMatrices` alone.