🐛 A need records the line it is actually written on - #1789
Merged
Conversation
``NeedDirective`` stored ``self.lineno``, which counts the lines of what the parser was handed rather than the lines of the file. Anything that puts text into a document shifts it for everything after -- Sphinx's ``rst_prolog``, and the ``insert_input`` of ``.. include::`` and ``.. list2need::`` -- the shifts compound, and the recorded line routinely landed past the end of the file. Take the line from ``SphinxDirective.get_source_info()`` instead, which resolves the counter back through the state machine. It is the accessor ``get_location()`` -- and so every warning this extension emits -- is already built on, so a need's recorded ``lineno`` and the line its warnings print are now the same number. ``docname`` and ``lineno_content`` are deliberately untouched. Closes #1349
A rendered ``pre_template``/``post_template`` exists in no file, so a warning raised inside it is anchored at the need's own directive. ``nested_parse`` resolves a nested parse's locations through the *enclosing* state machine (``abs_line_number()`` = ``line_offset + input_offset + 1``), so that anchor has to be given in the parser's line space -- the same space ``lineno_content`` is kept in, and not the resolved source line a need now records. Keep the unresolved number on ``NeedItemSourceDirective`` as ``parser_lineno`` (internal: not part of ``dict_repr``, so it never reaches ``needs.json``) and use it for those two offsets. Every other source kind still holds the parser's number in ``lineno`` itself, so the previous expression stays right for them and ``NeedItemSourceProtocol`` is unchanged. ``test_proper_warning`` already pins these locations; its values do not move. A new case builds the same fixture behind an ``rst_prolog`` and asserts the same five locations -- without this it reports 7 and 4 where the file says 9 and 6.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1789 +/- ##
==========================================
+ Coverage 86.87% 91.05% +4.17%
==========================================
Files 56 77 +21
Lines 6532 11593 +5061
==========================================
+ Hits 5675 10556 +4881
- Misses 857 1037 +180
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was referenced Aug 26, 2026
chrisjsewell
added a commit
that referenced
this pull request
Aug 26, 2026
chrisjsewell
added a commit
that referenced
this pull request
Aug 26, 2026
…dText (#1790) `list2need` renders every item into a need directive through a template and hands the result back to the parser with `state_machine.insert_input`. This replaces that step: the items are built with `add_need`, and a nested item is placed inside its parent. **The syntax and its parsing are untouched.** The same four regular expressions, the same level→type mapping, the same `(ID)` capture, the same `:delimiter:` split, the same `((option="value"))` extraction, the same `:tags:` merge, the same `:links-down:` walk, the same validation and the same eleven inputs that end the build. Only the generation mechanism changes. This is offered as the answer to the question in #1427 — *"can we save the syntax (mostly) but have a better implementation (without jinja)?"* ### What the step cost **Markdown.** myst-parser's mock state machine does not implement `insert_input`, so a `{list2need}` fence produced no needs at all — only an error naming a myst-parser internal. It now works: | | before | after | |---|---|---| | needs created | none | `MD-A`, `MD-B` | | nesting | — | `MD-B.parent_need == "MD-A"` | | diagnostics | `MockingError: MockStateMachine has not yet implemented attribute 'insert_input'` | none | **Line numbers (#1349, the `list2need` half).** `insert_input` advances the parser's flat line counter by the length of the generated text, for the rest of the file, and the error compounds with each list. For a document with two items on lines 10 and 11 and an ordinary `.. req::` on line 13: | need | written on | `lineno` before | after | |---|---|---|---| | `LN-BEFORE` | 4 | 4 | 4 | | `LN-A` | 10 | 14 | **10** | | `LN-B` | 11 | 20 | **11** | | `LN-AFTER` (an ordinary `.. req::`) | 13 | 27 | **13** | In the repository's own `doc_list2need` fixture, `NEED-Z` was recorded at line 45 of a 19-line file. The general case — a `lineno` shifted by `rst_prolog` or an `.. include::` — is a separate small PR, #1789. **Template artefacts.** An item written `()` with options lost them: suppressing the `:id:` left a blank line where it would have been, which ended the generated need's option block, so every option after it became body text. And a parent need's stored `content` was the generated reStructuredText of its children rather than its own text, which `needs.json` and any filter reading `content` saw. ### The one deliberate change to an input that worked Writing `()` used to suppress the `:id:` of the generated need, which sent it down the need directives' own id generator — a **different** function, which honours `needs_id_from_title` where `list2need`'s does not, hashes the *content* when the title is empty, and hashes the title *after* the `((option="value"))` area has been removed from it. One list could carry two id schemes at once, chosen by two characters of punctuation. The directive now derives every id itself, from the same input at the same point, so `()` means what no brackets at all means. Three kinds of item therefore get a different generated id: | item | before | after | |---|---|---| | `* ()Alpha title` with `needs_id_from_title = True` | `R_ALPHA` | `R_D1EC6`, the plain hash | | `* (). some content here` (no title) | `R_EBC29`, the hash of the **content** | `R_DA39A`, the hash of the empty title | | `* ()Title with empty parens ((status="open"))` | `R_6AFF7` | **`R_D7997`** — the id `* Title with empty parens ((status="open"))` already had | The third needs no configuration to reach and is the one to watch for; the changelog says so under Breaking changes, and the documentation carries a `versionchanged`. Everything else keeps the id it had, which the untouched id tests prove. ### Four inputs that were errors, and are now defined None of these can appear in a document that builds cleanly today, so nothing that works can regress; they are listed because each is a decision the template used to delegate to docutils and the directive now has to make. - **`((id="MY-ID"))`** names the need. The template wrote the derived id *and* the inline one as two `:id:` lines of one generated need, which docutils refused with `duplicate option "id"`, dropping the item. The id is applied before `:links-down:` is built, so the other items' links agree with it. An empty `((id=""))` is refused with a diagnostic rather than quietly replaced by the title hash. - **An inline option naming the same link field as `:links-down:`** — `((links="X"))` under `:links-down: links` — produced one corrupt link value (the template's `{%- for %}` glued the two field lines into `:links: C-2:links: X`) and an `unknown outgoing link` warning. The two sets are now merged. - **`((title_from_content="true"))`** is read as a need directive reads it. - **A child of a `((hide="true"))` item** is rendered at the level above it. A hidden need is taken out of the document once read, so nesting a child inside one would put its target where no page can reach it. ### Evidence - The rendered HTML of `tests/doc_test/doc_list2need/` is **byte-identical** before and after, all four pages, apart from the per-build random `SNCB-…` container ids. Its `needs.json` keeps all 16 ids, every title, status, tag, link and `parent_need`; only the linenos and the two parent `content` strings change. - The characterization suite added in #1788 is the safety net. The eleven build-aborting inputs and the `links-down` level-skip test pass **untouched** — this PR takes neither of those on. **Four** assertions are edited, each one pinning something the template did: the drifted linenos, the MyST failure (which inverts), the parent's generated-text content, and the second id function. A fifth prediction went the other way (below). - Two tests gain an assertion: the Markdown ones now pin the items' line numbers. Linenos inside an `{eval-rst}` fence were wrong too — that block was the only way to reach the directive from a Markdown document, and its needs were recorded at 10 and 16 for items written on 7 and 8. - `tests/__snapshots__/test_list2need.ambr` is rewritten: every `lineno` in it was wrong. - Six tests are added: the `()`-plus-options id, a directive written in an item's content, and one for each of the four inputs above. One assertion moved the other way. Removing the three-space indent the parser prepends to a `:`-continuation line looked like removing a workaround for the template — it is not: it is the only thing standing in for the indentation `lstrip` removes, and without it the options of a directive written in an item's content (the `rst-directives in lists` example in the documentation) land in the directive's own column and are dropped. It is kept, and now has a test. Refs #1427, Refs #1349.
This was referenced Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The change
NeedDirectivestoredself.lineno, which counts the lines of what the parser washanded rather than the lines of the file. Three perfectly ordinary things put text
into a document and shift that count for everything after: Sphinx prepends
rst_prologto every document it parses, and both docutils'.. include::and ourown
.. list2need::splice text in mid-parse withStateMachine.insert_input. Theshifts compound, so the recorded line routinely landed past the end of the file.
The line now comes from
SphinxDirective.get_source_info(), which is exactlystate_machine.get_source_and_line(self.lineno)— the accessorget_location(), andso every warning the directive itself emits, is already built on. The stored
linenonow agrees with the line our own warnings have been printing all along. Ona document with an
rst_prologand alist2need, for a need written onindex.rst:9:index.rst:9index.rst:9needs.jsonrecorded199The accessor returns
(None, None)when the state machine cannot map the line, inwhich case the old value is kept.
No warning location regresses. The only warning locations that move are the ones
this corrects — the sites that report at a need's stored
(docname, lineno)now namethe true line. Everything anchored through the directive's own location helper is
byte-identical to before. The one place that had to be taught the
difference is the rendered content of
:pre_template:/:post_template:. That contentexists in no file, so it is anchored at the need's own directive, and
nested_parseresolves a nested parse through the enclosing state machine — meaning the anchor has
to be given in the parser's line space, not in the resolved one a need now records.
NeedItemSourceDirectivetherefore also keeps the unresolved number (parser_lineno,internal — not in
needs.json), and_create_need_nodeuses it for those two offsets.tests/test_proper_warning.pypins these locations already; its values are unchanged,and a second test now builds the same fixture with an
rst_prologin front of itand asserts the same five locations, which without this would report 7 and 4 where
the file says 9 and 6.
What that looks like across the three causes
A 19-line document with all three, from the new test:
rst_prologonlyindex.rst:4.. include::index.rst:9.. list2need::index.rst:18Deliberately not changed
docnamestaysself.env.docname. Better lineno in needs JSON #1692 also derived it from the resolvedsource path; measured, that turns
docnameinto a filesystem path for any needinside an
.. include::d file and silently breaks the:need:hyperlinks pointingat it. Changing
docnamechanges link resolution, which is a separate questionfrom getting the line right.
lineno_contentstaysself.content_offset + 1. It is not read as a sourceline:
_create_need_nodehands it tonested_parseas an offset into the parser'sown line space, which is what lets a need nested inside another need's content report
sensible lines. Making the two agree wants a new field rather than a change to this
one; noted as a follow-up.
list2needgenerates still record their position inside thegenerated block rather than the line of the list item that produced them — that is
where their warnings already point, so it is no worse than before, and it is only
properly fixed by building those needs directly instead of round-tripping them
through the parser.
.. include::d file, inside alist2needblock, or in a Python docstring, the number now indexes a differentfile from the one
docnamenames, and it looks like an ordinary line of thatdocument — where the old value announced itself as wrong by sitting past the end of
the file. Both are wrong; the new one is wrong plausibly. It is deliberate: the new
number is the one that need's own warnings already print, and the real fix is for a
need to carry a source path of its own rather than for
docnameto change meaning.MockStateMachine.get_source_and_linereturns the line it is given, so
.mdneeds are byte-identical before and after.There is a test pinning that.
Tests
tests/test_need_lineno.py(new) — one project carrying all three shift sources,pinning the true line for every ordinary need directive, plus the Markdown no-op.
tests/test_list2need_behaviour.py— the lineno characterization test added in🧪 Pin list2need behaviour and correct its documentation #1788 now records the correct line for the need after the list, and keeps the
generated needs' offsets pinned as the remaining residue.
tests/__snapshots__/test_list2need.ambr— 16 generated-need linenos, each checkedagainst the generated text.
tests/test_proper_warning.py— its existing pins are untouched; a new case rebuildsthe same fixture behind an
rst_prologand asserts the same warning locations.Closes #1349.
Credit where it is due: #1692 (sebastiansetzer) identified this fix and its
direction. This PR carries its
need.pyline-number piece on its own.