Model editing¶
Small, verified mutations for UI inspectors: rename with a full
reference cascade (or an honest refusal), value edits that validate
unit semantics before mutating (a fake or wrong-dimension unit is
refused, never stored) and accept the compact quantity form the
inspector displays (17 g, and prefix-composed symbols like
17 mg – resolved through the model’s own unit vocabulary and
stored as the canonical bracket expression), documentation edits, and
a change-tracking seam – every operation keeps the textual export
parseable and at a fixpoint.
Model editing: small mutations with round-trip guarantees.
This is the seam UI inspectors mutate a model through: each operation
takes the model plus an element (or its ::-qualified name), validates
its input precisely, applies the smallest possible change, and leaves the
model in a state that still exports to parseable text
(longeron.to_sysml()), still resolves, and still validates. No
operation inserts or removes siblings of existing elements (set_doc
appends or edits in place), so index-path element ids – the
longeron.ecore projection ids derived from member positions –
stay stable across edits.
The operations:
import longeron
from longeron import edit
model = longeron.loads("package P { part def Vehicle; part v : Vehicle; }")
tracker = edit.track(model)
edit.rename(model, "P::Vehicle", "Car") # cascades into 'v : Car'
edit.set_doc(model, "P::v", "The prototype.")
tracker.dirty # True
tracker.changes # [(op, qname, detail), ...]
The honest-refusal rename philosophy. Renaming an element changes
the qualified names of its whole subtree, and every textual reference
in the model that reaches the element – or its descendants – through
the old name must be rewritten with it: typings, subsets, redefines,
connector ends, satisfy targets, exposes, imports, aliases, dependency
ends, metadata prefixes, and the name references inside owned
expressions. rename() resolves every reference site through the
same machinery longeron.validate() uses, rewrites exactly the
segments that resolve to the renamed element, and then re-resolves every
site to prove that nothing changed meaning. Whatever cannot be proven
safe is refused: references in positions that cannot be statically
resolved (member access on a computed value, e.g. seq#(1).mass) raise
EditError listing the offending sites, and a
rename that would silently re-bind any reference (name capture through
shadowing) is rolled back and refused. A rename that silently breaks
references is worse than no rename.
Value writes validate semantics, not just syntax.
set_attribute_value() applies the same philosophy to units: a new
expression carrying a measurement reference that does not resolve
(0.42 [SI::kgg]), or whose dimension contradicts the attribute’s
quantity typing (0.42 [SI::s] on a MassValue) or – when the
typing pins nothing – the current value’s own unit (0.42 [SI::s]
replacing 0.38 [SI::kg]), is refused before anything mutates –
through the very machinery validate’s dimensional lint uses, so the
edit seam and the lint share one truth. The compact quantity form the
inspector displays commits too: 17 g (or 17g) resolves the
symbol through the same derived unit table the display uses and is
rewritten to the canonical bracket expression (17 [SI::g]) for
storage; a symbol the model never names but that decomposes through the
model’s own prefix vocabulary (17 mg) rescales into the prefix’s
reference unit (0.017 [SI::g]) – model-derived, never invented –
and an ambiguous or unknown symbol is refused, never guessed.
validate=False is the documented escape hatch for deliberate
unchecked writes (including deliberate re-dimensioning).
Known blind spots, matching validate’s own: metadata value bodies
(level = 3; inside @Safety) resolve against the metadata
definition only one level deep, and references that reach an element
purely through an Alias are not rewritten when
the alias itself is renamed (the post-verification refuses such renames
rather than break them).
Change tracking. track() registers a Tracker in a
module-level weakref.WeakKeyDictionary keyed by the model object
itself, so trackers die with their models and no wrapper type is needed.
Every edit.* operation that mutates a tracked model appends a
Change record and fires the tracker’s callbacks; dirty is
simply “there are recorded changes since the last Tracker.mark_saved()”.
- class longeron.edit.Change(op: str, qname: str, detail: dict[str, Any])[source]¶
Bases:
NamedTupleOne recorded edit: unpacks as
(op, qname, detail).
- exception longeron.edit.EditError[source]¶
Bases:
SysMLErrorRaised when a model edit is invalid or would corrupt references.
longeron.editrefuses any mutation it cannot prove safe – a rename that would break or silently re-bind references raises this error (listing the affected sites) instead of corrupting the model.
- class longeron.edit.Tracker[source]¶
Bases:
objectAccumulates
Changerecords for one tracked model.dirtyis derived: True wheneverchangesis non-empty.mark_saved()clears both –changesis defined as “the edits since the last save”, which is exactly what an app needs to decide whether to prompt, and what to write into a commit message. Callbacks registered withon_change()fire synchronously, once per change, after the change is appended; exceptions propagate to the caller of the edit operation.
- longeron.edit.add_metadata(model, element_or_qname, typed_by, values=None)[source]¶
Append a metadata annotation (
@TypedBy { key = value; ... }).typed_byis the annotation’s (possibly qualified) metadata definition name;valuesmaps body member names to literal values (strings, numbers, booleans), each stored as aMetadataValuein the given order. The usage is appended to the target’s members – never inserted – so existing siblings keep their index-path element ids (theset_docrule). Like every operation here, a refusal (EditError) mutates nothing and records nothing on the tracker. Returns the new metadata usage.- Return type:
- longeron.edit.rename(model, element_or_qname, new_name)[source]¶
Rename an element, rewriting every textual reference that reaches it.
Validates that
new_nameis a legal name (non-empty, no::or.separators, no$, no control characters – anything else exports through the quoted-name form) and that no sibling already answers to it. Every reference site in the model is then resolved (with the same stdlib-aware machineryvalidateuses), the segments that resolve to the renamed element are rewritten, and the whole model is re-resolved to prove that every site still means what it meant before. Names in positions that cannot be statically resolved (member access on computed values) are refused up front when they mention the old name, and any silent re-binding (name capture) is rolled back – both raiseEditErrorlisting the affected references. See the module docstring for the philosophy.Renaming to the current name is a no-op. Returns the element.
- Return type:
- longeron.edit.set_attribute_value(model, attr_or_qname, text, *, validate=True)[source]¶
Set (or clear) a usage’s value from expression text.
textis parsed with the package’s expression parser (longeron.parse_expression()); a syntax error raisesEditErrorcarrying the parse diagnostics.Semantics are validated before anything mutates (the module’s honest-refusal philosophy, applied to values): every measurement reference the new expression carries must resolve against the model’s unit vocabulary –
0.42 [SI::kgg]raisesEditErrornaming the fake unit and the nearest real spellings – and when the usage’s typing pins a quantity dimension (payload : MassValue) the new value’s derived dimension must agree:0.42 [SI::s]on a mass-typed attribute is refused stating both dimensions, while0.42 [SI::g]is a real mass unit and passes. When the typing pins nothing (mass : Real) but the CURRENT value carries a resolvable unit, that unit’s dimension is the pin instead – replacing a[SI::kg]value with a[SI::s]one is refused stating both dimensions and thevalidate=Falseoverride (a deliberate re-dimensioning is legitimate, a silent one is corruption). A unit on a previously unit-less attribute is accepted as long as it resolves (adding units is legitimate), and a bare number always passes – its dimension is unknown, exactly asvalidatetreats it. The model is untouched by any refusal, and refused attempts record nothing on the tracker.validate=Falseskips this semantic gate for deliberate unchecked writes (syntax is still required).The compact quantity form the inspector displays commits as well:
17 g/17g– a number, optional space, one unit symbol – resolves the symbol through the model’s derived unit table (the very table the display reads) and stores the canonical bracket expression17 [SI::g]; the dimension gates above apply to it unchanged. A symbol the model does not name but that decomposes through the model’s own prefix vocabulary (17 mg) is rescaled into the prefix’s reference unit and stored as0.017 [SI::g]. An ambiguous decomposition is refused naming every candidate, and an unknown symbol is refused with the nearest real spellings. The rewrite is form normalization, so it applies undervalidate=Falsetoo.The existing value’s
default =/:=flags are preserved; a usage without a value gets a plain=binding.None(or blank text) removes the value entirely. Returns the usage.- Return type:
- longeron.edit.set_doc(model, element_or_qname, text)[source]¶
Create, update, or remove an element’s documentation.
With text: the element’s first
docmember is updated in place (keeping its member position, and collapsing any additionaldocmembers into it), or a new one is appended – never inserted – so existing siblings keep their index-path element ids. WithNoneor an empty string: thedocmembers are removed (the one edit that shrinks a member list; siblings after the doc get new index paths). Bodies are written in the canonical comment form (longeron.export.doc_comment_body()), which round-trips multi-line text through export/parse at a fixpoint. Per the comment convention, leading*decoration and per-line indentation are not part of the text. Returns the documentation element (Noneafter a removal).- Return type: