Editing scripts¶
The typed AST is not read-only. The same Script and Resource objects are
mutable: you set a field, add or rename a resource, or splice a mission-sequence command, and the
change is written back into the source. Editing keeps the byte-for-byte guarantee for every
untouched byte — only the span you touched is re-emitted.
from gmat_script import Script
script = Script.parse("""\
Create Spacecraft Sat
Sat.SMA = 7000
Sat.ECC = 0.01
BeginMissionSequence
Propagate DefaultProp(Sat) {Sat.ElapsedDays = 1}
""")
script.spacecraft["Sat"]["SMA"] = 8000 # raise the orbit
script.set_field("Sat", "ECC", 0.001) # the same edit, spelled as a method
print(script.to_source())
Create Spacecraft Sat
Sat.SMA = 8000
Sat.ECC = 0.001
BeginMissionSequence
Propagate DefaultProp(Sat) {Sat.ElapsedDays = 1}
How an edit is applied — and why it can't corrupt your script¶
A Script owns its source buffer. Each mutator computes byte-range edits over the current tree,
splices them in, re-parses the result, and commits — and that re-parse is a guard:
- it refuses to edit a script that already has syntax errors, and
- it rejects any edit whose result would not parse cleanly, raising
MutationErrorand leaving the source unchanged.
So a committed mutation always re-parses with zero ERROR nodes — you can never edit a valid script
into a broken one. Names and field paths are validated up front (a name carrying whitespace or a
newline would splice in extra statements that parse cleanly on their own, which the re-parse guard
could not catch), so those raise MutationError too:
from gmat_script import MutationError
try:
script.add_resource("Spacecraft", "Bad Name")
except MutationError as exc:
print(exc) # resource name 'Bad Name' is not a valid GMAT identifier
Every mutator returns the Script, so edits chain:
Editing fields¶
A Resource is a MutableMapping, so the dict operations all work and route through two underlying
edits — set and delete:
sat = script.spacecraft["Sat"]
sat["SMA"] = 7200 # rewrite the value in place (or append if the field is new)
sat.update({"TA": 0, "RAAN": 90})
del sat["ECC"] # remove every assignment to the field
sat.setdefault("INC", 28.5)
Setting a field rewrites the last assignment to it in place when it already exists, and otherwise
appends a canonical <resource>.<field> = <value> line next to the resource's configuration.
Deleting removes every assignment to that field; deleting a field that was never assigned raises
KeyError. The method forms — script.set_field(resource, field, value) and
script.delete_field(resource, field) — do the same thing without a Resource handle.
The value you write is formatted with the same canonical emitter the formatter uses, so a Python value maps to one chosen GMAT form (see value coercion for the inverse):
from gmat_script import Array, ObjectRef
sat["SMA"] = 7000 # -> Sat.SMA = 7000
sat["DragArea"] = 15.5 # -> Sat.DragArea = 15.5
sat["DateFormat"] = "UTCGregorian" # -> Sat.DateFormat = 'UTCGregorian'
sat["Tanks"] = [ObjectRef("FuelTank")] # -> Sat.Tanks = {FuelTank}
sat["OrbitColor"] = Array((1, 0, 0)) # -> Sat.OrbitColor = [1 0 0]
Use ObjectRef to write a bare reference ({FuelTank}, not the string {'FuelTank'}), and
RawValue as the escape hatch for a computed right-hand side. A value that cannot be written
without corrupting the script is rejected at emission — a non-finite float, a string carrying a
single quote or newline, or an empty / newline-bearing bare value all raise ValueError.
Editing resources¶
add_resource appends Create <type> <name> (plus any fields) to the configuration, landing just
before BeginMissionSequence (or at end of file when there is no marker):
Create Spacecraft Sat
Sat.SMA = 7000
Sat.ECC = 0.01
Create ImpulsiveBurn TOI
TOI.Element1 = 0.5
TOI.Axes = VNB
BeginMissionSequence
Propagate DefaultProp(Sat) {Sat.ElapsedDays = 1}
The edit is a minimal splice — it inserts the new lines and leaves every other byte exactly where it was, which is why the blank line stays put rather than moving to sit before the marker. Run the formatter afterwards to normalise the surrounding blank lines into canonical form.
remove_resource(name) drops a resource's Create (or just its name, for a multi-name declaration)
and every configuration assignment rooted at it. References to it elsewhere are left as-is — use
rename_resource to rewrite those. Adding a name that already exists raises MutationError.
Renaming, and the reference-rewrite¶
rename_resource(old, new) renames a resource and rewrites references to it by default:
Create Spacecraft MainSat
MainSat.SMA = 7000
Create ImpulsiveBurn TOI
TOI.Element1 = 0.5
BeginMissionSequence
Maneuver TOI(MainSat)
Propagate DefaultProp(MainSat) {MainSat.ElapsedDays = 1}
Report rf MainSat.Earth.SMA
The rewrite is best-effort over the textual reference forms, and deliberately scoped. It rewrites each identifier whose syntactic role is an object reference — the root of a dotted path, the head of an array-index / function call, a bare operand or argument or list element, and the declaration name. It deliberately leaves alone:
- field-name segments — the
.Earth.SMAafter the root inMainSat.Earth.SMA, so a field that happens to share the old name is never disturbed; - the
Createtype — renaming an object never touches a coincidentally-equal type token; - identifiers inside an opaque
BeginScript … EndScriptbody — that body is a single raw token, not parsed, so references there are not rewritten.
Pass update_references=False to rename only the declaration, leaving every call site untouched —
which means the old references now point at a name that no longer exists, so reach for it only when
you intend exactly that:
script.rename_resource("Sat", "MainSat", update_references=False)
# -> `Create Spacecraft MainSat`, but `Sat.SMA = 7000` and `Maneuver TOI(Sat)` keep `Sat`
Renaming onto a name that already exists raises MutationError.
Editing mission-sequence commands¶
Commands are spliced by position in the mission sequence. insert_command(index, text) takes raw
command source and inserts it as its own line before index (or at the end):
script.insert_command(0, "Toggle rf On")
script.insert_command(len(script.mission_sequence), "Stop")
The companions are remove_command(index) (drops the command — a whole block, if it is one),
replace_command(index, text) (rewrites the command in place, preserving its indentation and
trailing newline), and move_command(from_index, to_index) (reorders without re-emitting the
command). Inserting into an empty sequence needs a BeginMissionSequence marker to insert after; an
out-of-range index raises IndexError.
Re-read the sequence after a command edit
mission_sequence is a snapshot of the current tree. After an edit that changes it, re-read the
attribute rather than reusing command handles taken before the edit. Resource handles, by
contrast, are live cursors — a handle kept across an edit reflects it, and reading a resource
that was removed or renamed out from under it raises KeyError.
Next steps¶
- The formatter — normalise layout after a batch of edits.
- API reference — the full mutation surface on
ScriptandResource. - The
examples/directory has runnable before/after scripts for a field edit and a resource rename.