Skeleton sections: every section a test report needs, in order
(report feature skeleton_sections).
A test report is read by people who look for each part of it in the
same place every time, so a section that is missing tells them nothing:
they cannot tell "nothing happened" from "nobody wrote it down". With
the feature on, the report is laid out on the twelve sections of
SECTIONS and closed with the end mark, whatever the project holds.
What Visual Dynamics generates falls under the sections it answers
(place): the pass/fail box and the opening description under the
Summary, the geometry, the photographs and the channel table under
Instrumentation, the closing reading guide under the Conclusion, and
every other figure and its text under Results.
What only the author can write (the article, the setup and method, the
chronology, the deviations, the discussion, the conclusion's wording)
is never generated. Each such section holds the author's own words, a
text block marked with the section's key ({'kind': 'text', 'text':
..., 'section': key}), and until that block says something the page
shows the section as owed: NOT YET WRITTEN, with what belongs there.
A section with nothing to report says so in one step (write(...,
none=True)): "No deviations. No anomalies. No nonconformances." The
discussion carries who interpreted (by), and the conclusion carries
the sentence that the results relate only to the item tested.
check lists what is still owed, for the export to ask about and for
a script (Project.report_check). write is the one place a
section's words change; the editor's pane and Project.report_section
both call it.
The arrangement is made on the page being rendered (arrange,
finish), not on the report: the report's own blocks keep their order,
a section block is drawn only while the feature is on, and turning the
feature off gives back the page it was.
Classes:
| Name |
Description |
Section |
One section of the skeleton.
|
Owed |
A section check found still owed: which, and why.
|
Functions:
| Name |
Description |
section |
|
blocks_of |
The indices of report's blocks holding section key's words,
|
words |
What the author has written in section key, stripped; '' while
|
write |
Set what section key says, and return the index of its block.
|
state |
'written', 'owed' or 'quiet' (a filled section with nothing of
|
instrumentation |
Whether a block shows what was tested and how it was instrumented:
|
place |
The section each generated block falls under.
|
check |
The sections of report still owed, in order: each section the
|
arrange |
The page's blocks laid out on the skeleton.
|
under |
arrange's blocks with extra added at the end of section
|
finish |
Draw each head arrange made, in place, once its words are HTML:
|
Classes
Section
dataclass
Section(key: str, number: int, title: str, description: str, required: bool = True, none: str = '')
One section of the skeleton.
required sections are the author's to write and are owed until
they say something. The others (Instrumentation, Results) are
filled by what the project generates and are owed only when nothing
falls under them. none is what one step writes when the section
has nothing to report, or '' for a section that always has
something (every report describes the article it tested).
Attributes:
| Name |
Type |
Description |
heading |
str
|
'9. Deviations, Anomalies and Nonconformances'.
|
Attributes
heading
property
'9. Deviations, Anomalies and Nonconformances'.
Owed
dataclass
Owed(key: str, number: int, title: str, reason: str)
A section check found still owed: which, and why.
Functions:
section
The section named key.
Raises ValueError for a key that is not a section, naming the
sections, and TypeError for one that is not a string.
Source code in src/visualdynamics/report/skeleton.py
| def section(key: Any) -> Section:
"""The section named `key`.
Raises `ValueError` for a key that is not a section, naming the
sections, and `TypeError` for one that is not a string.
"""
if not isinstance(key, str):
raise TypeError(f'a skeleton section is named by its key, a str, '
f'not {type(key).__name__}')
found = BY_KEY.get(key)
if found is None:
raise ValueError(f'unknown skeleton section {key!r}; the sections '
f'are: {", ".join(BY_KEY)}')
return found
|
blocks_of
blocks_of(report: Any, key: str) -> list[int]
The indices of report's blocks holding section key's words,
in order. Normally one; none until the section is first written.
Source code in src/visualdynamics/report/skeleton.py
| def blocks_of(report: Any, key: str) -> list[int]:
"""The indices of `report`'s blocks holding section `key`'s words,
in order. Normally one; none until the section is first written."""
return [index for index, block in enumerate(report.blocks)
if block.get('section') == key]
|
words
words(report: Any, key: str) -> str
What the author has written in section key, stripped; '' while
it is owed.
Source code in src/visualdynamics/report/skeleton.py
| def words(report: Any, key: str) -> str:
"""What the author has written in section `key`, stripped; '' while
it is owed."""
texts = (str(report.blocks[index].get('text', '')).strip()
for index in blocks_of(report, key))
return '\n\n'.join(text for text in texts if text)
|
write
write(report: Any, key: str, text: str | None = None, *, none: bool = False, by: str | None = None) -> int
Set what section key says, and return the index of its block.
The section's block is added at the end of the report the first
time (where it sits among the blocks does not matter: the page lays
the sections out in order). text replaces the section's words.
none writes the section's own "nothing to report" wording, as one
step. by is who made the interpretation, for the discussion only;
an empty by clears it. With none of them, the block is made if it
is missing and nothing else changes.
Refused, before anything changes: an unknown section; text and
none together; none for a section that always has something to
say; by for a section other than the discussion.
Raises:
| Type |
Description |
ValueError
|
For a refusal; the message names the section and the reason.
|
TypeError
|
When key, text or by is not a string.
|
Source code in src/visualdynamics/report/skeleton.py
| def write(report: Any, key: str, text: str | None = None, *,
none: bool = False, by: str | None = None) -> int:
"""Set what section `key` says, and return the index of its block.
The section's block is added at the end of the report the first
time (where it sits among the blocks does not matter: the page lays
the sections out in order). `text` replaces the section's words.
`none` writes the section's own "nothing to report" wording, as one
step. `by` is who made the interpretation, for the discussion only;
an empty `by` clears it. With none of them, the block is made if it
is missing and nothing else changes.
Refused, before anything changes: an unknown section; `text` and
`none` together; `none` for a section that always has something to
say; `by` for a section other than the discussion.
Raises
------
ValueError
For a refusal; the message names the section and the reason.
TypeError
When `key`, `text` or `by` is not a string.
"""
found = section(key)
if text is not None and not isinstance(text, str):
raise TypeError(f'{found.heading}: the text is a str, not '
f'{type(text).__name__}')
if by is not None and not isinstance(by, str):
raise TypeError(f'{found.heading}: by is a str, not '
f'{type(by).__name__}')
if none and text is not None:
raise ValueError(f'{found.heading}: give text or none, not both')
if none and not found.none:
raise ValueError(f'{found.heading} cannot be set to None: every '
'report has something to say here')
if by is not None and key != ATTRIBUTED:
raise ValueError(f'{found.heading} has no interpretation to '
f'attribute; by belongs to '
f'{BY_KEY[ATTRIBUTED].heading}')
held = blocks_of(report, key)
if held:
index = held[0]
else:
report.add({'kind': 'text', 'text': '', 'section': key})
index = report.num_blocks - 1
block = report.blocks[index]
if none:
block['text'] = found.none
elif text is not None:
block['text'] = text
if by is not None:
if by.strip():
block['by'] = by.strip()
else:
block.pop('by', None)
return index
|
state
state(found: Section, text: str, by: str = '', generated: bool = False) -> str
'written', 'owed' or 'quiet' (a filled section with nothing of
the author's in it, which shows its heading only).
text is what the author wrote, by who interpreted, and
generated whether anything the project generates falls under the
section. The discussion, written without saying by whom, is
'unattributed'.
Source code in src/visualdynamics/report/skeleton.py
| def state(found: Section, text: str, by: str = '',
generated: bool = False) -> str:
"""'written', 'owed' or 'quiet' (a filled section with nothing of
the author's in it, which shows its heading only).
`text` is what the author wrote, `by` who interpreted, and
`generated` whether anything the project generates falls under the
section. The discussion, written without saying by whom, is
'unattributed'.
"""
if text:
if found.key == ATTRIBUTED and text != found.none and not by:
return 'unattributed'
return 'written'
if found.required or not generated:
return 'owed'
return 'quiet'
|
instrumentation
instrumentation(block: Mapping[str, Any], objects: Mapping[str, Any], object_groups: Sequence[Mapping[str, Any]] | None = None) -> bool
Whether a block shows what was tested and how it was instrumented:
a photograph, the geometry or a DOF scene (a scene with no shapes),
or a channel table.
Source code in src/visualdynamics/report/skeleton.py
| def instrumentation(block: Mapping[str, Any], objects: Mapping[str, Any],
object_groups: Sequence[Mapping[str, Any]] | None = None
) -> bool:
"""Whether a block shows what was tested and how it was instrumented:
a photograph, the geometry or a DOF scene (a scene with no shapes),
or a channel table."""
from ..core.channel_table import ChannelTable
from ..core.report import resolve_binding
kind = block.get('kind')
if kind == 'photo':
return True
if kind == 'scene':
return not block.get('shapes')
if kind == 'table':
name = resolve_binding(block.get('source'), objects, object_groups)
return isinstance(objects.get(name or ''), ChannelTable)
return False
|
place
place(entries: Sequence[tuple[Mapping[str, Any], bool, bool]]) -> list[str]
The section each generated block falls under.
entries is the report's blocks in their order, other than the
section blocks, each as (block, is a drawn figure, shows the
instrumentation). The blocks are read in the sections their ##
headings make. A section holding the pass/fail box goes under the
Summary, and so does a section of text only that comes before every
figure (the description that opens a generated report). A section
with a drawn photograph, geometry, DOF scene or channel table goes
under Instrumentation. A section of text only after every figure
(the reading guide that closes it) goes under the Conclusion.
Everything else is Results.
Source code in src/visualdynamics/report/skeleton.py
| def place(entries: Sequence[tuple[Mapping[str, Any], bool, bool]]
) -> list[str]:
"""The section each generated block falls under.
`entries` is the report's blocks in their order, other than the
section blocks, each as (block, is a drawn figure, shows the
instrumentation). The blocks are read in the sections their `## `
headings make. A section holding the pass/fail box goes under the
Summary, and so does a section of text only that comes before every
figure (the description that opens a generated report). A section
with a drawn photograph, geometry, DOF scene or channel table goes
under Instrumentation. A section of text only after every figure
(the reading guide that closes it) goes under the Conclusion.
Everything else is Results.
"""
groups: list[list[int]] = []
for position, (block, _figure, _shows) in enumerate(entries):
if _heading(block) or not groups:
groups.append([])
groups[-1].append(position)
figures = [position for position, (_b, figure, _s) in enumerate(entries)
if figure]
first = figures[0] if figures else len(entries)
last = figures[-1] if figures else -1
out = [''] * len(entries)
for group in groups:
members = [entries[position] for position in group]
if any(block.get('kind') == 'verdict' for block, _f, _s in members):
where = 'summary'
elif any(figure and shows for _b, figure, shows in members):
where = 'instrumentation'
elif not any(figure for _b, figure, _s in members) \
and group[-1] < first:
where = 'summary'
elif not any(figure for _b, figure, _s in members) \
and group[0] > last:
where = 'conclusion'
else:
where = 'results'
for position in group:
out[position] = where
return out
|
check
check(report: Any, objects: Mapping[str, Any], object_groups: Sequence[Mapping[str, Any]] | None = None) -> tuple[Owed, ...]
The sections of report still owed, in order: each section the
author has not written, a filled section with nothing in it, and a
discussion that does not say by whom. Empty when nothing is owed.
A generated block counts as filling its section when its bindings
resolve in objects (a text block always does), which is what the
reading page draws.
Raises ValueError when the report does not have the skeleton
sections enabled: there is then no skeleton to check it against,
and an empty list would read as a report complete.
Source code in src/visualdynamics/report/skeleton.py
| def check(report: Any, objects: Mapping[str, Any],
object_groups: Sequence[Mapping[str, Any]] | None = None
) -> tuple[Owed, ...]:
"""The sections of `report` still owed, in order: each section the
author has not written, a filled section with nothing in it, and a
discussion that does not say by whom. Empty when nothing is owed.
A generated block counts as filling its section when its bindings
resolve in `objects` (a text block always does), which is what the
reading page draws.
Raises `ValueError` when the report does not have the skeleton
sections enabled: there is then no skeleton to check it against,
and an empty list would read as a report complete.
"""
from .features import enabled
if FEATURE not in enabled(report):
raise ValueError(f'report {getattr(report, "title", "")!r} does not '
'have Skeleton sections enabled, so there is no '
'skeleton to check it against')
# what is drawn is what the page draws (`_build_block` returning
# something), not whether the block's bindings resolve: a block
# that resolves and still draws nothing counted as filling its
# section here while the page showed the section not yet written
# (2026-10-28)
from ..units import DEFAULT_SYSTEM
from . import _build_block
from .prediction import build as build_prediction
active = enabled(report)
def drawn(block):
if block.get('kind') == 'prediction':
return ('prediction' in active and build_prediction(
block, objects, DEFAULT_SYSTEM, object_groups) is not None)
return _build_block(block, objects, DEFAULT_SYSTEM,
object_groups) is not None
entries = []
for block in report.blocks:
if 'section' in block or not drawn(block):
continue
figure = _figure(block.get('kind'))
entries.append((block, figure, figure and instrumentation(
block, objects, object_groups)))
filled = set(place(entries))
owed = []
# with identity and issue on, the identification section is the
# identity block: owed while a field it needs is not entered
from .identity import FEATURE as IDENTITY
from .identity import missing
identified = IDENTITY in enabled(report)
for found in SECTIONS:
if identified and found.key == 'identification':
gaps = missing(report)
if gaps:
owed.append(Owed(found.key, found.number, found.title,
f'{", ".join(gaps)} not entered'))
continue
held = blocks_of(report, found.key)
by = str(report.blocks[held[0]].get('by', '')) if held else ''
now = state(found, words(report, found.key), by,
found.key in filled)
if now in ('owed', 'unattributed'):
reason = (UNATTRIBUTED if now == 'unattributed' else
NOT_WRITTEN if found.required else EMPTY)
owed.append(Owed(found.key, found.number, found.title, reason))
return tuple(owed)
|
arrange
arrange(built: Sequence[dict[str, Any]], origins: Sequence[Mapping[str, Any]], held: Mapping[str, Sequence[tuple[int, Mapping[str, Any], dict[str, Any]]]], objects: Mapping[str, Any], object_groups: Sequence[Mapping[str, Any]] | None = None, edit: bool = False) -> list[dict[str, Any]]
The page's blocks laid out on the skeleton.
built is the page's blocks as built, in the report's order, and
origins the report block each was built from. held is each
section's own blocks, by key, as (index in the report, the block,
the block built: its text with live references resolved). Returns the blocks in skeleton order: for each section, a
head (its heading and the author's words, or the owed card) and
then what falls under it; and last the end mark. A head carries
skeleton for finish to draw once the text is rendered. On the
edit page, a head carries the index of the section's block, or
'section:' while it has none, so a click on it opens it.
Source code in src/visualdynamics/report/skeleton.py
| def arrange(built: Sequence[dict[str, Any]],
origins: Sequence[Mapping[str, Any]],
held: Mapping[str, Sequence[tuple[int, Mapping[str, Any],
dict[str, Any]]]],
objects: Mapping[str, Any],
object_groups: Sequence[Mapping[str, Any]] | None = None,
edit: bool = False) -> list[dict[str, Any]]:
"""The page's blocks laid out on the skeleton.
`built` is the page's blocks as built, in the report's order, and
`origins` the report block each was built from. `held` is each
section's own blocks, by key, as (index in the report, the block,
the block built: its text with live references resolved). Returns the blocks in skeleton order: for each section, a
head (its heading and the author's words, or the owed card) and
then what falls under it; and last the end mark. A head carries
`skeleton` for `finish` to draw once the text is rendered. On the
edit page, a head carries the index of the section's block, or
'section:<key>' while it has none, so a click on it opens it.
"""
entries = []
for block, origin in zip(built, origins, strict=True):
figure = _figure(block.get('kind'))
entries.append((origin, figure, figure and instrumentation(
origin, objects, object_groups)))
where = place(entries)
out: list[dict[str, Any]] = []
for found in SECTIONS:
mine = [block for block, at in zip(built, where, strict=True)
if at == found.key]
own = held.get(found.key, ())
texts = [str(built_text.get('text', '')).strip()
for _index, _block, built_text in own]
text = '\n\n'.join(text for text in texts if text)
by = str(own[0][1].get('by', '')) if own else ''
generated = any(block.get('kind') != 'unbound' for block in mine)
head: dict[str, Any] = {
'kind': 'text', 'text': text,
'skeleton': {'key': found.key, 'by': by,
'state': state(found, text, by, generated),
# the discussion names who interpreted, unless
# it says there is no interpretation
'attribute': (found.key == ATTRIBUTED and bool(text)
and text != found.none)}}
if edit:
head['index'] = own[0][0] if own else f'section:{found.key}'
out.append(head)
out.extend(mine)
out.append({'kind': 'text', 'text': '', 'skeleton': {'key': 'end'}})
return out
|
under
under(blocks: Sequence[dict[str, Any]], key: str, extra: Sequence[dict[str, Any]]) -> list[dict[str, Any]]
arrange's blocks with extra added at the end of section
key: after the author's words and whatever already falls under
it, before the next section's head (or the end mark). For blocks a
feature generates into a section, such as the data index under the
record. Raises ValueError for a key that is not a section.
Source code in src/visualdynamics/report/skeleton.py
| def under(blocks: Sequence[dict[str, Any]], key: str,
extra: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
"""`arrange`'s blocks with `extra` added at the end of section
`key`: after the author's words and whatever already falls under
it, before the next section's head (or the end mark). For blocks a
feature generates into a section, such as the data index under the
record. Raises `ValueError` for a key that is not a section."""
section(key)
out = list(blocks)
start = next((at for at, block in enumerate(out)
if block.get('skeleton', {}).get('key') == key), None)
if start is None:
raise ValueError(f'{BY_KEY[key].heading} is not on this page')
stop = next((at for at in range(start + 1, len(out))
if 'skeleton' in out[at]), len(out))
return out[:stop] + list(extra) + out[stop:]
|
finish
finish(blocks: Sequence[dict[str, Any]], end: str = END) -> None
Draw each head arrange made, in place, once its words are HTML:
the heading, then the author's words, the owed card or nothing;
the discussion's attribution and the conclusion's sentence. end
is the last line ('End of amendment.' for an amendment, from the
identity feature).
Source code in src/visualdynamics/report/skeleton.py
| def finish(blocks: Sequence[dict[str, Any]], end: str = END) -> None:
"""Draw each head `arrange` made, in place, once its words are HTML:
the heading, then the author's words, the owed card or nothing;
the discussion's attribution and the conclusion's sentence. `end`
is the last line ('End of amendment.' for an amendment, from the
identity feature)."""
for block in blocks:
meta = block.pop('skeleton', None)
if meta is None:
continue
if meta['key'] == 'end':
block['html'] = f'<p class="endmark">{html.escape(end)}</p>'
continue
found = BY_KEY[meta['key']]
parts = [f'<h2 class="skeleton">{html.escape(found.heading)}</h2>']
written = block.get('html', '')
if meta['state'] == 'owed':
parts.append(f'<div class="owed"><strong>{OWED}</strong> '
f'{html.escape(found.description)}</div>')
elif meta['state'] in ('written', 'unattributed'):
if meta['attribute']:
who = html.escape(meta['by']) if meta['by'] else \
'<span class="owed">not stated</span>'
parts.append(f'<p class="interpretation">Interpretation, '
f'by {who}.</p>')
parts.append(written)
if found.key == 'conclusion':
parts.append(f'<p class="tested">{html.escape(TESTED)}</p>')
# joined with nothing between: a text block keeps its white
# space (`.text`), and a line between the heading and the card
# would draw as a gap
block['html'] = ''.join(part for part in parts if part)
|