What works inside imported need content¶
When you import needs with .. needimport::, ubCode reads each need’s
content and treats it as markup, not as a dead string:
cross-references resolve, need parts are extracted, and the body renders.
It does not treat it as your markup. The content came from another project’s export, so a handful of constructs are deliberately refused. This page lists every one of them, what you see instead, and how to lift the restriction where that is possible.
You will not have to find this page by accident. Every refusal reports itself
at the .. needimport:: directive, and where a setting would allow the
construct the message names that setting. If something in imported content did
not render, look at the warning first — it should tell you the answer, and this
page tells you why the rule exists.
Note
Nothing here applies to need content you write yourself. These rules are about content that arrives from outside the project.
The short version¶
Ordinary requirement bodies are unaffected. Emphasis, lists, tables with inline
data, admonitions, code blocks and inline diagrams work exactly as they do in
your own files, and so do cross-references — a :need:, :ref:, :doc:,
:term: or :any: written inside imported content resolves against your
project, whether or not anything else in it happens to reference the same
target. A sphinx-design cross-reference (a button-ref, or a card’s
whole-card :link:) resolves too, whichever ref-type it uses — including
the default, any.
A reference that is relative to the page it is written on — a relative
:doc:, or an any reference that names a document — resolves against the
page whose needimport produced the card. The same imported content can
therefore link somewhere slightly different on two different pages, which is
the one place an import is not fully portable; every other target space (need
IDs, labels, glossary terms, intersphinx objects) is project-wide and answers
the same everywhere.
That only arises when the same content really does reach two pages — two needs
with different IDs, or one source imported twice under different
:id_prefix: values. Importing the same ID on two pages does not do it:
the second is a duplicate, reported as such, and only one card is rendered.
One cross-reference family is the exception: an explicit intersphinx reference
(:external:…:) inside imported content does not resolve, and renders as
plain text. Nothing else in the family behaves that way; if you need it, say so
on the tracking issue.
What is refused is a small set of constructs that reach outside the content — either to a file on disk, or into the parse itself.
What you see on the page¶
Each .. needimport:: renders one card per imported need, at the
directive’s own place in the document, in ascending ID order. A card looks like
a need you wrote yourself: the same header, the same metadata table (with
needextend overlays and incoming links applied), and the need’s content
as its body.
Imported needs are also clickable from anywhere a need renders — a
:need: role, a needlist row, a needtable cell, a link in another
need’s metadata — and land on the card, on the page whose needimport
produced it.
If the same ID arrives from two imports, only one card is rendered: the one the
project already resolved the ID to. The other site shows nothing for that ID,
and the existing duplicate-ID warning tells you about the clash. A locally
authored need always wins over an imported one; between two imports, the
earlier needimport wins.
- In the editor preview
A rendered preview shows the first 50 needs per import and says so, so a page importing thousands stays responsive while you type. Raise, lower or disable it with
needs.import_preview_max. This is a preview limit only:ubc build htmlalways renders every imported need, because a truncated build would silently drop content from the published site.The same key caps a .. src-trace:: site, which is the other directive that renders need cards the project generated rather than the author wrote. One knob covers both, so a project that traces a large source tree and one that imports a large
needs.jsonare tuned in the same place.- Which parser renders the content
Each need’s content is parsed as the markup it declares: a need whose
doctypefield says.mdrenders as Markdown, even imported into an RST project. The declared suffix is matched against the parsers your project configures under[parse.parsers]— the same routing that decides how your own files parse, with the longest matching suffix winning — so the export says what the content is, while the parser and all of its settings are always your project’s.Most exports declare nothing, and that is fine: with
doctypeabsent or empty, the content is parsed with the flavour of the file the.. needimport::itself is written in. Adoctypethat no configured parser claims produces one warning per source, naming the unclaimed suffix, and falls back the same way — so the parser in use is never a silent guess.Two parsers can also claim the same suffix, told apart only by the directory their
includeglobs name. Your own files are then routed by path, but adoctypecarries no path, so it cannot choose between them: the content is parsed with the first parser in name order, and you get one warning per source (needs.import.doctype_ambiguous) naming every tied parser and the one that won. Give the parsers globs implying different suffixes if imported content should use a particular one.If one source’s needs declare two different tied
doctypes, only the first is reported — the fix is the same edit to your parser table either way.- If a
:filter:matches nothing An import whose
:filter:selects none of the needs its source offered contributes nothing, and says so — once per source, with both counts. This is an informational message (needs.import.filter_empty), not a warning, so it does not failubc check: a filter that matches nothing this week may match something next.It is worth knowing why a plausible-looking filter can match nothing. The expression is applied to each need’s raw JSON in the source — before tags are added and long before
needextendruns, matching Sphinx-Needs — and a field the export never sets is read from the export’s own schema defaults, which are typicallynull. Sostatus == "open"over a status-less export matches nothing rather than reporting an error. Check the filter’s field names and values against the JSON.A source that was empty to begin with is never blamed on its filter.
- If an
:ids:entry names nothing An
:ids:list that names a need the source does not contain reports it (needs.import.ids_unmatched), naming the unmatched ids — once per source, with a handful named and a count of the rest. The other ids still import normally, which is exactly why this is worth reporting: one typo among five is invisible in the output.This is a warning, where the filter message above is only a note. The difference is deliberate: a filter is a predicate, and matching nothing may simply be the truth today; an id is a name, and one that resolves to nothing is a mistake in either your directive or the source.
- If a need’s
idis not its key A
needs.jsonkeys each need by its id, and an export usually repeats the id inside the need as"id". Where the two disagree, the key names the need (behind your:id_prefix:, if any) and the"id"is ignored, as in a Sphinx build with Sphinx-Needs 9.0.0 or later (earlier versions take the"id"). ubCode says so once per such need (needs.import), naming both spellings — for every need of the source, not only the ones the import selects, because the fault is in the file.
Refused, but unlockable¶
What |
What you see instead |
Why |
|---|---|---|
|
The generic directive placeholder, plus a warning naming the setting.
A Markdown |
An imported reference names a path that means something in the exporting project. Resolving it against your pages would let a third party’s JSON point at any file you publish. |
Diagrams from a file ( |
Same as above. Inline diagram bodies are unaffected and keep rendering. |
Only the file form reaches outside the content. A diagram written inline is self-contained, so there is nothing to police. |
Any other construct that names an asset — a |
The construct still renders; the asset reference is dropped, with the same warning naming the setting. |
The rule is about the reference, not the directive that carries it: every asset reference in imported content goes through one policy, so a newly supported directive cannot quietly become an exception. |
To allow them, name the source and set allow_assets on it. The project-wide
key stays off; the per-source entry is the unlock:
[[needs.import_sources]]
json_path = "specs/needs.json" # or json_url = "https://…"
allow_assets = true # ← this is the unlock
assets_root = "_assets/specs" # if the assets are not beside the JSON
needs.import_allow_assets flips the project-wide default the other way, if
you would rather opt out per source than in.
The policy belongs to the source, not to the .. needimport:: directive.
Two directives on different pages that import the same file must resolve its
assets the same way, so the setting lives with the file it describes. It is
also why this is configuration rather than a directive option: needimport
is a directive Sphinx-Needs also defines, and an option only ubCode understood
would break the same document under sphinx-build.
Once allowed, the references render as your own do: the file is copied into the
site’s _images/ directory, the <img> gets a real src, a diagram file
is read and drawn, and editing the file re-renders the pages that show it. There
is no second mechanism — an unlocked asset is an asset.
Important
What you are granting, stated plainly. allow_assets = true gives that
source the same authority over your source tree that an image you wrote
yourself has. A relative reference resolves against the directory holding the
source’s own needs.json, but an absolute /logo.png resolves against
the project’s source directory — so the source can reference any file published under
your source directory, and that file is copied into the built site.
The unlock does not create a new capability; it extends an existing one to a
new principal. For a source you do not control, set assets_root as well:
that narrows the reach to one subtree, in both reference forms.
- Where imported assets resolve
Against the JSON’s own directory, not the importing page’s. That is a deliberate difference from Sphinx-Needs, which resolves them against whichever document did the importing — so the same file resolves differently on every page that imports it. Keying on the JSON’s location means an exporting project can ship assets next to its
needs.jsonand have every consumer find them.assets_rootre-roots this if your layout differs.A
/-prefixed path is your project’s own rule, unchanged: it resolves against the project’s source directory. Withassets_rootset, both forms resolve inside it — which is how you narrow a third-party source to one subtree of your tree.- What the mount can reach, and what it cannot
Exactly the directory it names, and below. A reference that climbs out of it with
..is refused with a warning that names the reference and the directory it was checked against, in every spelling of..— it is never resolved. Everything else behaves as an authored reference does, including a missing file, which reports the same “could not read” warning your own missing image would.Two limits worth knowing. Without
assets_root, the base for an absolute/xis the source directory itself (see the note above), so “outside the mount” means outside your source tree rather than outside the JSON’s folder. And a symlink inside the mount resolves wherever it points, exactly as it would for an image you wrote yourself — if that matters for a source you do not control, do not link out of the tree.- Three cases need
assets_root A remote source (
json_url) has no local directory to resolve against, and ubCode does not fetch remote assets — so vendor them into your project and pointassets_rootat them. The same applies to a localneeds.jsonthat sits outside the source directory: there is no directory inside your project for a relative reference to mean.The third is a local
needs.jsonthat is inside the source directory but whose folder name cannot be used as a base: names containing whitespace, a backslash,:,#or?are read differently by different parts of the build, so resolving against one would be a guess. Rather than mount at a folder you did not name, ubCode refuses and asks for an explicitassets_root.In all three the warning says which one it is, and names
assets_root.assets_rootitself must stay inside the source directory, and is held to the same naming rule — it is a base too. One that leads outside, or that is spelled ambiguously, is reported against your configuration once, naming the entry by its index, and that source’s asset references are refused rather than resolved somewhere unexpected.- In an untrusted workspace, the preview refuses them
Trusting a workspace and allowing a source’s assets are two different statements, and both are required before the editor reads a file because imported content asked it to. In an untrusted workspace the rendered preview therefore shows the placeholder and says the session refused it — the same posture
rawtakes there.ubc build htmlis unaffected: running a build is itself an explicit act.- A URL in imported content stays a URL — but only a real one
An
.. image:: https://…(or adata:URI) inside imported content is emitted exactly as it is written, the same as in your own files: ubCode never fetches it, and readers’ browsers load it from wherever it points. If that matters for a source you do not control, do not unlock it.A protocol-relative reference (
//host/file.png) is treated as a project path instead, and so is checked against the mount like any other path. It has no working meaning in imported content, and reading it as a URL would let a third party’s data choose a host your readers fetch from.
Refused, with no setting to lift it¶
What |
What you see instead |
Why |
|---|---|---|
|
The generic directive placeholder, plus a warning. |
These pull bytes into the parse, where they can open a directive block, define a substitution or end a table. A wrong image is visible and recoverable; a wrong include silently changes what the surrounding markup means, and the page gives no clue where it came from. |
|
Same as above. |
Same reason — it reads a file into the parse. |
|
Same as above. |
Enabling |
Need directives inside imported content (a |
The placeholder plus a warning. No need is created. |
That nested need already exists as its own entry in the same export, so running it again would only re-create a need you are already importing. Sphinx-Needs ends up in the same place, but by way of a duplicate-ID warning. |
Nested |
The placeholder plus a warning. Nothing is read. |
A recursive import. Sphinx-Needs has no depth limit and no cycle guard here, so a self-referential chain is stopped only by chance; refusing at the first level bounds it by construction. |
Two differences you will not get a warning about¶
These are absences rather than refusals — there is no construct to attach a message to. They are listed so you can find them when something looks off.
- Substitutions do not resolve
|version|,|release|,|today|, and your ownrst_prologsubstitutions all expand to nothing. Imported content is parsed on its own, with no host document around it, which is what makes an import mean the same thing on every page that uses it. (Cross-references are not affected: a relative:doc:, or ananyreference naming a document, resolves against the page whoseneedimportproduced the card.):id_prefix:does not rewrite the content textReferences resolve correctly: a
:need:role inside a prefixed import that names a sibling by its raw ID resolves to the prefixed need, and becomes a working link. That holds for every ID the source contributed, including one that lost an ID clash to a need defined elsewhere. What differs is an ID mentioned in prose — ubCode leaves it as written, where Sphinx-Needs rewrites it. ubCode applies the prefix when resolving a reference rather than by substituting text, so it also cannot mangleREQ_1insideREQ_10, which the Sphinx-Needs implementation notes as a known weakness of its own approach.
See also
Coming from Sphinx-Needs for the rest of the migration story.
Test results from sphinx-test-reports¶
Added in version 0.37.0.
sphinx-test-reports turns JUnit-style XML test reports into needs.
Its converter, the test-reports command of the ub-test-reports package (pip install ub-test-reports),
does the same without Sphinx: it writes one need per test case to a needs.json,
and ubCode imports that file like any other.
Use this route when your reports come out of CI
and you want the test cases next to your requirements — in tables, queries and traceability links.
Note
The test-file, test-suite and test-case directives read a report directly,
one need each, or one per suite and case with :auto_suites: / :auto_cases:,
the test-results and test-env tables render one,
and the test-report directive renders a report through its template
(Test reports: the directives).
Converting the reports and importing the result is the other route, shown below.
The examples below are one project:
ubproject.toml at its root, the XML reports in reports/, the pages in docs/.
Declare the section¶
ubCode and the converter both read the [test_reports] section of ubproject.toml,
so the converter’s settings are written once.
This is the ubproject.toml of the example project:
[project]
name = "demo"
[source]
dir = "docs"
# Present, even empty, this table gives ubCode the testcase type and its fields:
# ubCode does not read conf.py, so loading sphinx-test-reports there is not enough.
[test_reports]
extra_options = ["TestType"]
[test_reports.build.needs]
project = "demo"
link_properties = { PartiallyVerifies = "links" }
extra_options exports an XML property as a field of the same name.
link_properties turns a property into a link field —
here a PartiallyVerifies="REQ_1, REQ_2" property becomes links to REQ_1 and REQ_2;
ubCode reads it as the converter’s spelling of property_link_types —
a deprecated spelling, used here because the converter does not read property_link_types yet
(see Test reports).
The section also gives the project the testcase need type and every field the converter writes,
TestType included, so nothing else is declared.
Without it, every test case is refused on import (Invalid need type: testcase).
Convert the reports¶
Run the converter from the project root:
$ test-reports build needs reports/*.xml --output build/test_needs.json
reading [test_reports] from /path/to/demo/ubproject.toml
Every <testcase> becomes a need of type testcase.
The converter finds the ubproject.toml by walking up from the working directory,
and takes project, extra_options and link_properties from its [test_reports] section.
Pass the reports as relative paths:
each test case records its report in the file field exactly as given.
Import the needs¶
Either import them on a page, where each test case renders as a card at the directive’s place (the path is relative to the page):
.. needimport:: ../build/test_needs.json
Or load them for the whole project, with no cards, from ubproject.toml
(json_path is relative to the ubproject.toml):
[[needs.external_needs]]
json_path = "build/test_needs.json"
base_url = "https://ci.example.com/test-results"
What then works¶
The imported test cases are needs like any other.
A needtable lists the failures:
.. needtable::
:filter: type == 'testcase' and result == 'failed'
:columns: id, suite, case, time, result_text
ubc query filter finds them from the command line:
$ ubc query filter "type == 'testcase' and result == 'failed'" --field id --field suite --field time
id suite time
-------------------------------------------------- --------- -----------
testcase__MathTest__Subtraction_srmht MathTest 0.002
testcase__fail_test_output__fail_test_output_kddlp Linux-c++ 0.000576548
testcase__test_example__test_error_iidoh pytest62 0.018
testcase__test_example__test_fail_ipenb pytest62 0.002
4 needs found.
A Cypher query counts them by result:
$ ubc query cypher "MATCH (t:testcase) RETURN t.result, count(*) ORDER BY t.result"
t.result count(*)
-------- --------
disabled 1
failed 4
passed 6
skipped 15
4 rows returned.
And the links made from link_properties connect each test case to the requirements it verifies:
$ ubc query cypher "MATCH (t:testcase)-[:links]->(r:req) RETURN t.id, t.result, r.id"
t.id t.result r.id
---------------------------------- -------- -----
testcase__MathTest__Addition_hcuyy passed REQ_1
testcase__MathTest__Addition_hcuyy passed REQ_2
2 rows returned.
Things to know¶
[[needs.external_needs]]wants abase_urlor an absolutetarget_urlWith no
base_urland notarget_url, the source is reported (needs.external):no 'base_url' is configured for this external source, so its needs have no 'external_url' and every reference to them (tables, lists, the :need: role) renders unlinked
The converter records no page for a test case, so with
base_urlalone each link is<base_url>/__error__.html#<id>, as in a Sphinx build; atarget_urltemplate such as"#{{ need.id }}"chooses the rest of the URL. Run with--remote-urland--commit, the converter records each test case’s source URL inremote_url, andtarget_url = "{{ need.remote_url }}"links each test case to it: atarget_urlthat renders an absolute URL (scheme://…or//…) is used as it is, in ubCode as in a Sphinx build, sobase_urlcan be""(ubCode gives no link for one with ajavascript:,vbscript:ordata:scheme, or a control character) (a Sphinx build needs the key, and accepts it empty). Any other render is appended tobase_url; with nobase_url, or an empty one, it gives no link, and ubCode reports how many needs that left unlinked.timeandcase_lineare stringsThe converter writes every value as a string (
"time": "0.002"), so a numeric comparison such astime > 0.1fails withUnsupported type for number comparison; variable: time.[needs.fields.time] schema = { type = "number" }converts the imported values (a baretype = "number"is not a[needs.fields]key and is silently ignored). Writing them as numbers is an upstream change: sphinx-needs#1954.- A mixed-case field cannot be a
needtablecolumn :columns:entries are matched in lower case, in ubCode as in a Sphinx build, so aTestTypecolumn renders empty. Filters and queries read the field as written (TestType == 'requirements-based'); for a table column, export the property under a lower-case name.- The converter’s ids are lowercase
Ids such as
testcase__MathTest__Subtraction_srmhtare accepted on import, but the same spelling on a need directive you write yourself is refused by the defaultid_regex(needs.id_regex:Need directive ID "…" does not match regex '^[A-Z0-9_]{5,}').