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 html always 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.json are tuned in the same place.

Which parser renders the content

Each need’s content is parsed as the markup it declares: a need whose doctype field says .md renders 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 doctype absent or empty, the content is parsed with the flavour of the file the .. needimport:: itself is written in. A doctype that 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 include globs name. Your own files are then routed by path, but a doctype carries 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 fail ubc 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 needextend runs, matching Sphinx-Needs — and a field the export never sets is read from the export’s own schema defaults, which are typically null. So status == "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 id is not its key

A needs.json keys 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

.. image:: and .. figure::

The generic directive placeholder, plus a warning naming the setting. A Markdown ![alt](pic.png) shows its alt text instead.

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 (.. uml:: flow.puml, .. mermaid:: x.mmd)

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 .. video:: source or :poster:, a sphinx-design card’s :img-top: / :img-bottom:

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.json and have every consumer find them. assets_root re-roots this if your layout differs.

A /-prefixed path is your project’s own rule, unchanged: it resolves against the project’s source directory. With assets_root set, 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 /x is 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 point assets_root at them. The same applies to a local needs.json that sits outside the source directory: there is no directory inside your project for a relative reference to mean.

The third is a local needs.json that 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 explicit assets_root.

In all three the warning says which one it is, and names assets_root.

assets_root itself 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 raw takes there. ubc build html is 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 a data: 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

.. include:: and .. literalinclude::

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.

.. csv-table:: with :file: (inline CSV data is fine)

Same as above.

Same reason — it reads a file into the parse.

.. raw::, even with raw_enabled = true

Same as above.

Enabling raw says you trust your own files. It does not say anything about a third party’s JSON, so the two decisions are kept separate.

Need directives inside imported content (a .. spec:: in an imported body)

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 .. needimport:: or .. src-trace::

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 own rst_prolog substitutions 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 an any reference naming a document, resolves against the page whose needimport produced the card.)

:id_prefix: does not rewrite the content text

References 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 mangle REQ_1 inside REQ_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 a base_url or an absolute target_url

With no base_url and no target_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_url alone each link is <base_url>/__error__.html#<id>, as in a Sphinx build; a target_url template such as "#{{ need.id }}" chooses the rest of the URL. Run with --remote-url and --commit, the converter records each test case’s source URL in remote_url, and target_url = "{{ need.remote_url }}" links each test case to it: a target_url that renders an absolute URL (scheme://… or //…) is used as it is, in ubCode as in a Sphinx build, so base_url can be "" (ubCode gives no link for one with a javascript:, vbscript: or data: scheme, or a control character) (a Sphinx build needs the key, and accepts it empty). Any other render is appended to base_url; with no base_url, or an empty one, it gives no link, and ubCode reports how many needs that left unlinked.

time and case_line are strings

The converter writes every value as a string ("time": "0.002"), so a numeric comparison such as time > 0.1 fails with Unsupported type for number comparison; variable: time. [needs.fields.time] schema = { type = "number" } converts the imported values (a bare type = "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 needtable column

:columns: entries are matched in lower case, in ubCode as in a Sphinx build, so a TestType column 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_srmht are accepted on import, but the same spelling on a need directive you write yourself is refused by the default id_regex (needs.id_regex: Need directive ID "…" does not match regex '^[A-Z0-9_]{5,}').