@h2loop/autosar 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/agents/autosar-integrator.md +34 -2
- package/package.json +4 -1
- package/plugins/index.js +53 -23
- package/python/ecuc_engine/__init__.py +1 -1
- package/python/ecuc_engine/add_signal.py +351 -0
- package/python/ecuc_engine/arxml_edit.py +387 -0
- package/python/ecuc_engine/audit.py +328 -48
- package/python/ecuc_engine/cli.py +114 -5
- package/python/ecuc_engine/codegen/__init__.py +185 -0
- package/python/ecuc_engine/codegen/binding.py +210 -0
- package/python/ecuc_engine/codegen/bindings/sip_standin.json +111 -0
- package/python/ecuc_engine/codegen/cdiff.py +395 -0
- package/python/ecuc_engine/codegen/cwriter.py +150 -0
- package/python/ecuc_engine/codegen/emit.py +395 -0
- package/python/ecuc_engine/codegen/handles.py +89 -0
- package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
- package/python/ecuc_engine/codegen/model.py +259 -0
- package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
- package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
- package/python/ecuc_engine/codegen/modules/dem.py +22 -0
- package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
- package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
- package/python/ecuc_engine/codegen/registry.py +133 -0
- package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
- package/python/ecuc_engine/context.py +481 -0
- package/python/ecuc_engine/csrc.py +189 -0
- package/python/ecuc_engine/defs.py +30 -0
- package/python/ecuc_engine/find.py +162 -0
- package/python/ecuc_engine/gen.py +105 -0
- package/python/ecuc_engine/history.py +146 -0
- package/python/ecuc_engine/model.py +4 -1
- package/python/ecuc_engine/probe.py +229 -0
- package/python/ecuc_engine/rte_api.py +935 -0
- package/python/ecuc_engine/rules/__init__.py +5 -3
- package/python/ecuc_engine/rules/mem.py +84 -0
- package/python/ecuc_engine/rules/nvm.py +131 -0
- package/python/ecuc_engine/rules/pdur.py +87 -0
- package/python/ecuc_engine/rules/system.py +162 -0
- package/python/ecuc_engine/sysmodel.py +152 -0
- package/python/ecuc_engine/validate.py +15 -2
- package/python/ecuc_engine/verify_rte.py +445 -0
- package/python/pyproject.toml +1 -1
- package/skills/autosar/SKILL.md +47 -1
- package/skills/autosar/references/codegen.md +30 -0
- package/skills/autosar/references/project-layout.md +7 -1
- package/skills/autosar/references/runnable-code.md +50 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""Structured, in-place edits of non-ECUC ARXML (System/ECU extract, SWCs, interfaces).
|
|
2
|
+
|
|
3
|
+
Ops (paths are AUTOSAR paths of identifiables; tags are XML element names):
|
|
4
|
+
{"op": "add", "parent_path": "/Communication/ISignals", "element": "I-SIGNAL", "short_name": "X",
|
|
5
|
+
"values": {"LENGTH": 8, "SYSTEM-SIGNAL-REF": "->/Communication/SystemSignals/X",
|
|
6
|
+
"INIT-VALUE": {"NUMERICAL-VALUE-SPECIFICATION": {"VALUE": 0}}}}
|
|
7
|
+
{"op": "set", "path": "/Communication/Pdus/P/X", "element": "START-POSITION", "value": 40}
|
|
8
|
+
{"op": "remove", "path": "/Communication/ISignals/X"}
|
|
9
|
+
|
|
10
|
+
- The aggregating collection (ELEMENTS, I-SIGNAL-TO-PDU-MAPPINGS, ...) is inferred from the AUTOSAR schema
|
|
11
|
+
(autosar-data) when not given; children are written in schema order.
|
|
12
|
+
- Every *-REF / *-TREF gets its DEST from the referenced element's actual tag ("->/path"), looked up in all
|
|
13
|
+
loaded ARXML (config, extract, and the elements added in the same call). A target that does not exist is an
|
|
14
|
+
error, unless given explicitly as {"ref": "/path", "dest": "TAG"}.
|
|
15
|
+
- Bytes outside the inserted/changed spans are untouched; the file is strictly re-loaded with autosar-data
|
|
16
|
+
afterwards and nothing is written if any op or check fails.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import difflib
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from .model import Project
|
|
26
|
+
from .xmltree import Node, escape, escape_attr, line_indent, parse
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ArxmlEditError(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _ad():
|
|
34
|
+
try:
|
|
35
|
+
import autosar_data as ad # type: ignore
|
|
36
|
+
return ad
|
|
37
|
+
except Exception: # noqa: BLE001
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _version(data: bytes):
|
|
42
|
+
ad = _ad()
|
|
43
|
+
if ad is None:
|
|
44
|
+
return None
|
|
45
|
+
m = re.search(rb"AUTOSAR_([0-9A-Za-z_-]+)\.xsd", data[:2000])
|
|
46
|
+
name = "AUTOSAR_" + (m.group(1).decode().replace("-", "_") if m else "00054")
|
|
47
|
+
return getattr(ad.AutosarVersion, name, None) or ad.AutosarVersion.AUTOSAR_00054
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FileTree:
|
|
51
|
+
def __init__(self, path: str, data: bytes):
|
|
52
|
+
self.path, self.data = path, data
|
|
53
|
+
self.root = parse(data)
|
|
54
|
+
self.by_path: dict = {}
|
|
55
|
+
self._index(self.root, "")
|
|
56
|
+
self.nl = b"\r\n" if b"\r\n" in data[:4096] else b"\n"
|
|
57
|
+
|
|
58
|
+
def _index(self, n: Node, prefix: str):
|
|
59
|
+
sn = n.short_name
|
|
60
|
+
p = f"{prefix}/{sn}" if sn is not None else prefix
|
|
61
|
+
if sn is not None:
|
|
62
|
+
self.by_path.setdefault(p, n)
|
|
63
|
+
for c in n.children:
|
|
64
|
+
self._index(c, p)
|
|
65
|
+
|
|
66
|
+
def chain(self, node: Node) -> list:
|
|
67
|
+
out = []
|
|
68
|
+
while node is not None:
|
|
69
|
+
out.append(node.tag)
|
|
70
|
+
node = node.parent
|
|
71
|
+
return list(reversed(out))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _etype(tags: list, version):
|
|
75
|
+
"""ElementType for a tag chain from the AUTOSAR root (None without autosar-data)."""
|
|
76
|
+
ad = _ad()
|
|
77
|
+
if ad is None or version is None:
|
|
78
|
+
return None
|
|
79
|
+
m = ad.AutosarModel()
|
|
80
|
+
t = m.root_element.element_type
|
|
81
|
+
for tag in tags[1:]:
|
|
82
|
+
t = t.find_sub_element(tag, version)
|
|
83
|
+
if t is None:
|
|
84
|
+
return None
|
|
85
|
+
return t
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _order(etype) -> list:
|
|
89
|
+
return [s.element_name for s in etype.sub_elements_spec] if etype is not None else []
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ArxmlEditor:
|
|
93
|
+
def __init__(self, project: Project):
|
|
94
|
+
self.p = project
|
|
95
|
+
self.files: dict = {}
|
|
96
|
+
self.original: dict = {}
|
|
97
|
+
self.added_tags: dict = {} # path -> tag of elements added in this call
|
|
98
|
+
|
|
99
|
+
# ---------- files / lookup ----------
|
|
100
|
+
def tree_for(self, path: str) -> tuple:
|
|
101
|
+
"""(FileTree, node) holding the identifiable ``path``."""
|
|
102
|
+
for ft in self.files.values():
|
|
103
|
+
if path in ft.by_path:
|
|
104
|
+
return ft, ft.by_path[path]
|
|
105
|
+
hit = self.p.identifiables.get(path)
|
|
106
|
+
f = hit[2] if hit and len(hit) > 2 else None
|
|
107
|
+
if f is None:
|
|
108
|
+
for cand in self.p.sources.input + self.p.sources.config:
|
|
109
|
+
ft = self.load(cand)
|
|
110
|
+
if path in ft.by_path:
|
|
111
|
+
return ft, ft.by_path[path]
|
|
112
|
+
raise ArxmlEditError(f"no ARXML element at {path}")
|
|
113
|
+
ft = self.load(f)
|
|
114
|
+
if path not in ft.by_path:
|
|
115
|
+
raise ArxmlEditError(f"{path} not found in {self.p.rel(f)}")
|
|
116
|
+
return ft, ft.by_path[path]
|
|
117
|
+
|
|
118
|
+
def load(self, f: str) -> FileTree:
|
|
119
|
+
if f not in self.files:
|
|
120
|
+
with open(f, "rb") as fh:
|
|
121
|
+
data = fh.read()
|
|
122
|
+
self.original.setdefault(f, data)
|
|
123
|
+
self.files[f] = FileTree(f, data)
|
|
124
|
+
return self.files[f]
|
|
125
|
+
|
|
126
|
+
def patch(self, ft: FileTree, start: int, end: int, repl: bytes):
|
|
127
|
+
data = ft.data[:start] + repl + ft.data[end:]
|
|
128
|
+
self.files[ft.path] = FileTree(ft.path, data)
|
|
129
|
+
|
|
130
|
+
def dest_of(self, target: str) -> Optional[str]:
|
|
131
|
+
if target in self.added_tags:
|
|
132
|
+
return self.added_tags[target]
|
|
133
|
+
hit = self.p.identifiables.get(target)
|
|
134
|
+
if hit:
|
|
135
|
+
return hit[0]
|
|
136
|
+
for ft in self.files.values():
|
|
137
|
+
n = ft.by_path.get(target)
|
|
138
|
+
if n is not None:
|
|
139
|
+
return n.tag
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
# ---------- rendering ----------
|
|
143
|
+
def render(self, tag: str, short_name: Optional[str], values, etype, version, depth: int) -> list:
|
|
144
|
+
lines = []
|
|
145
|
+
attrs = ""
|
|
146
|
+
if isinstance(values, dict) and "@attrs" in values:
|
|
147
|
+
attrs = "".join(f' {k}="{escape_attr(str(v))}"' for k, v in values["@attrs"].items())
|
|
148
|
+
if not isinstance(values, dict):
|
|
149
|
+
return [(depth, self.leaf(tag, values))]
|
|
150
|
+
lines.append((depth, f"<{tag}{attrs}>"))
|
|
151
|
+
if short_name is not None:
|
|
152
|
+
lines.append((depth + 1, f"<SHORT-NAME>{escape(short_name)}</SHORT-NAME>"))
|
|
153
|
+
order = _order(etype)
|
|
154
|
+
keys = [k for k in values if not k.startswith("@") and k != "SHORT-NAME"]
|
|
155
|
+
if order:
|
|
156
|
+
unknown = [k for k in keys if k not in order]
|
|
157
|
+
if unknown:
|
|
158
|
+
raise ArxmlEditError(f"{tag} has no sub-element(s) {unknown} in this AUTOSAR schema; allowed: "
|
|
159
|
+
f"{', '.join(o for o in order if o != 'SHORT-NAME')[:600]}")
|
|
160
|
+
keys.sort(key=order.index)
|
|
161
|
+
for k in keys:
|
|
162
|
+
v = values[k]
|
|
163
|
+
for one in (v if isinstance(v, list) else [v]):
|
|
164
|
+
sub = etype.find_sub_element(k, version) if etype is not None else None
|
|
165
|
+
if isinstance(one, dict) and "ref" not in one:
|
|
166
|
+
sn = one.get("SHORT-NAME")
|
|
167
|
+
lines.extend(self.render(k, sn, one, sub, version, depth + 1))
|
|
168
|
+
else:
|
|
169
|
+
lines.append((depth + 1, self.leaf(k, one)))
|
|
170
|
+
lines.append((depth, f"</{tag}>"))
|
|
171
|
+
return lines
|
|
172
|
+
|
|
173
|
+
def leaf(self, tag: str, v) -> str:
|
|
174
|
+
if isinstance(v, dict) and "ref" in v:
|
|
175
|
+
target, dest = str(v["ref"]), v.get("dest") or self.dest_of(str(v["ref"]))
|
|
176
|
+
if not dest:
|
|
177
|
+
raise ArxmlEditError(f"{tag} -> {target}: target does not exist (add it first, or give "
|
|
178
|
+
"{{\"ref\": ..., \"dest\": \"TAG\"}})")
|
|
179
|
+
return f'<{tag} DEST="{escape_attr(dest)}">{escape(target)}</{tag}>'
|
|
180
|
+
if isinstance(v, str) and v.startswith("->"):
|
|
181
|
+
target = v[2:].strip()
|
|
182
|
+
if not (tag.endswith("-REF") or tag.endswith("-TREF") or tag.endswith("-IREF")):
|
|
183
|
+
raise ArxmlEditError(f"{tag} is not a reference element but got {v!r}")
|
|
184
|
+
dest = self.dest_of(target)
|
|
185
|
+
if not dest:
|
|
186
|
+
raise ArxmlEditError(f"{tag} -> {target}: no element with that path in the loaded ARXML (add it first "
|
|
187
|
+
"in the same call, or pass {\"ref\": path, \"dest\": TAG})")
|
|
188
|
+
return f'<{tag} DEST="{escape_attr(dest)}">{escape(target)}</{tag}>'
|
|
189
|
+
if isinstance(v, bool):
|
|
190
|
+
v = "true" if v else "false"
|
|
191
|
+
if (tag.endswith("-REF") or tag.endswith("-TREF")) and isinstance(v, str) and v.startswith("/"):
|
|
192
|
+
return self.leaf(tag, "->" + v)
|
|
193
|
+
return f"<{tag}>{escape(str(v))}</{tag}>"
|
|
194
|
+
|
|
195
|
+
def emit(self, ft: FileTree, lines: list, base: bytes, unit: bytes) -> bytes:
|
|
196
|
+
return ft.nl.join(base + unit * d + s.encode("utf-8") for d, s in lines)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def unit_of(ft: FileTree, node: Node) -> bytes:
|
|
200
|
+
if node.children:
|
|
201
|
+
ci = line_indent(ft.data, node.children[0].start) or b""
|
|
202
|
+
ni = line_indent(ft.data, node.start) or b""
|
|
203
|
+
if len(ci) > len(ni) and ci.startswith(ni):
|
|
204
|
+
return ci[len(ni):]
|
|
205
|
+
return b" "
|
|
206
|
+
|
|
207
|
+
# ---------- ops ----------
|
|
208
|
+
def op_add(self, op: dict):
|
|
209
|
+
parent_path, tag = op.get("parent_path"), op.get("element")
|
|
210
|
+
if not parent_path or not tag:
|
|
211
|
+
raise ArxmlEditError("add needs parent_path and element")
|
|
212
|
+
ft, parent = self.tree_for(parent_path.rstrip("/"))
|
|
213
|
+
version = _version(ft.data)
|
|
214
|
+
ptype = _etype(ft.chain(parent), version)
|
|
215
|
+
coll = op.get("collection")
|
|
216
|
+
if coll is None and ptype is not None:
|
|
217
|
+
if ptype.find_sub_element(tag, version) is None:
|
|
218
|
+
for c in _order(ptype):
|
|
219
|
+
ct = ptype.find_sub_element(c, version)
|
|
220
|
+
if ct is not None and ct.find_sub_element(tag, version) is not None:
|
|
221
|
+
coll = c
|
|
222
|
+
break
|
|
223
|
+
if coll is None:
|
|
224
|
+
raise ArxmlEditError(f"{tag} cannot be placed under {parent.tag} {parent_path} in this schema")
|
|
225
|
+
elif coll is None:
|
|
226
|
+
coll = "ELEMENTS" if parent.tag == "AR-PACKAGE" else None
|
|
227
|
+
sn = op.get("short_name")
|
|
228
|
+
new_path = f"{parent_path.rstrip('/')}/{sn}" if sn else None
|
|
229
|
+
if new_path and (new_path in ft.by_path or self.p.identifiables.get(new_path)):
|
|
230
|
+
raise ArxmlEditError(f"{new_path} already exists")
|
|
231
|
+
ctype = ptype.find_sub_element(coll, version) if (ptype is not None and coll) else ptype
|
|
232
|
+
etype = ctype.find_sub_element(tag, version) if ctype is not None else None
|
|
233
|
+
if new_path:
|
|
234
|
+
self.added_tags[new_path] = tag
|
|
235
|
+
lines = self.render(tag, sn, op.get("values") or {}, etype, version, 0)
|
|
236
|
+
unit = self.unit_of(ft, parent)
|
|
237
|
+
if not coll: # a direct child: place it at its schema position
|
|
238
|
+
ind = (line_indent(ft.data, parent.children[0].start) if parent.children else None) or b""
|
|
239
|
+
self._insert_leaf(ft, parent, tag, self.emit(ft, lines, ind, unit).lstrip())
|
|
240
|
+
return new_path
|
|
241
|
+
holder = parent.child(coll)
|
|
242
|
+
if holder is not None and holder.children:
|
|
243
|
+
last = holder.children[-1]
|
|
244
|
+
ind = line_indent(ft.data, last.start) or b""
|
|
245
|
+
self.patch(ft, last.end, last.end, ft.nl + self.emit(ft, lines, ind, unit))
|
|
246
|
+
return new_path
|
|
247
|
+
if holder is not None: # empty collection element
|
|
248
|
+
ind = line_indent(ft.data, holder.start) or b""
|
|
249
|
+
body = [(0, f"<{holder.tag}>")] + [(d + 1, s) for d, s in lines] + [(0, f"</{holder.tag}>")]
|
|
250
|
+
self.patch(ft, holder.start, holder.end, self.emit(ft, body, ind, unit).lstrip())
|
|
251
|
+
return new_path
|
|
252
|
+
# create the collection at its schema position among the parent's children
|
|
253
|
+
body = [(0, f"<{coll}>")] + [(d + 1, s) for d, s in lines] + [(0, f"</{coll}>")]
|
|
254
|
+
order = _order(ptype)
|
|
255
|
+
after = set(order[order.index(coll) + 1:]) if coll in order else set()
|
|
256
|
+
ind = (line_indent(ft.data, parent.children[0].start) if parent.children else None) or \
|
|
257
|
+
((line_indent(ft.data, parent.start) or b"") + unit)
|
|
258
|
+
before = next((c for c in parent.children if c.tag in after), None)
|
|
259
|
+
if before is not None:
|
|
260
|
+
self.patch(ft, before.start, before.start, self.emit(ft, body, ind, unit).lstrip() + ft.nl + ind)
|
|
261
|
+
else:
|
|
262
|
+
last = parent.children[-1]
|
|
263
|
+
self.patch(ft, last.end, last.end, ft.nl + self.emit(ft, body, ind, unit))
|
|
264
|
+
return new_path
|
|
265
|
+
|
|
266
|
+
def own_child(self, node: Node, tag: str) -> Optional[Node]:
|
|
267
|
+
stack = list(node.children)
|
|
268
|
+
while stack:
|
|
269
|
+
c = stack.pop(0)
|
|
270
|
+
if c.tag == tag:
|
|
271
|
+
return c
|
|
272
|
+
if c.short_name is None:
|
|
273
|
+
stack.extend(c.children)
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
def op_set(self, op: dict):
|
|
277
|
+
path, tag = op.get("path"), op.get("element")
|
|
278
|
+
if not path or not tag or "value" not in op:
|
|
279
|
+
raise ArxmlEditError("set needs path, element and value")
|
|
280
|
+
ft, node = self.tree_for(path.rstrip("/"))
|
|
281
|
+
leaf = self.own_child(node, tag)
|
|
282
|
+
text = self.leaf(tag, op["value"]).encode("utf-8")
|
|
283
|
+
if leaf is None:
|
|
284
|
+
self._insert_leaf(ft, node, tag, text)
|
|
285
|
+
return
|
|
286
|
+
self.patch(ft, leaf.start, leaf.end, text)
|
|
287
|
+
|
|
288
|
+
def _insert_leaf(self, ft: FileTree, node: Node, tag: str, text: bytes):
|
|
289
|
+
version = _version(ft.data)
|
|
290
|
+
order = _order(_etype(ft.chain(node), version))
|
|
291
|
+
if order and tag not in order:
|
|
292
|
+
raise ArxmlEditError(f"{node.tag} has no sub-element {tag} in this schema")
|
|
293
|
+
after = set(order[order.index(tag) + 1:]) if tag in order else set()
|
|
294
|
+
ind = line_indent(ft.data, node.children[0].start) if node.children else b""
|
|
295
|
+
before = next((c for c in node.children if c.tag in after), None)
|
|
296
|
+
if before is not None:
|
|
297
|
+
self.patch(ft, before.start, before.start, text + ft.nl + (ind or b""))
|
|
298
|
+
else:
|
|
299
|
+
last = node.children[-1]
|
|
300
|
+
self.patch(ft, last.end, last.end, ft.nl + (ind or b"") + text)
|
|
301
|
+
|
|
302
|
+
def op_remove(self, op: dict):
|
|
303
|
+
path = (op.get("path") or "").rstrip("/")
|
|
304
|
+
ft, node = self.tree_for(path)
|
|
305
|
+
start = node.start
|
|
306
|
+
if line_indent(ft.data, start) is not None:
|
|
307
|
+
nl = ft.data.rfind(b"\n", 0, start)
|
|
308
|
+
start = nl if nl >= 0 else start
|
|
309
|
+
self.patch(ft, start, node.end, b"")
|
|
310
|
+
|
|
311
|
+
# ---------- checks ----------
|
|
312
|
+
def check(self) -> list:
|
|
313
|
+
"""References in touched files resolve, with DEST equal to the target's tag; schema re-load."""
|
|
314
|
+
problems = []
|
|
315
|
+
known = dict(self.p.identifiables)
|
|
316
|
+
for ft in self.files.values():
|
|
317
|
+
for path, n in ft.by_path.items():
|
|
318
|
+
known[path] = (n.tag, n.line, ft.path)
|
|
319
|
+
for f, ft in self.files.items():
|
|
320
|
+
if ft.data == self.original.get(f):
|
|
321
|
+
continue
|
|
322
|
+
before = set(re.findall(rb"<([A-Z0-9-]+-T?REF)\b[^>]*>([^<]*)<", self.original[f]))
|
|
323
|
+
for n in ft.root.iter():
|
|
324
|
+
if not (n.tag.endswith("-REF") or n.tag.endswith("-TREF")):
|
|
325
|
+
continue
|
|
326
|
+
tgt = n.text.strip()
|
|
327
|
+
if (n.tag.encode(), tgt.encode()) in before:
|
|
328
|
+
continue # pre-existing reference, untouched
|
|
329
|
+
dest = n.attrs.get("DEST")
|
|
330
|
+
hit = known.get(tgt)
|
|
331
|
+
if not dest:
|
|
332
|
+
problems.append(f"{self.p.rel(f)}:{n.line} {n.tag} -> {tgt}: missing DEST attribute")
|
|
333
|
+
if hit is None:
|
|
334
|
+
problems.append(f"{self.p.rel(f)}:{n.line} {n.tag} -> {tgt}: target does not exist")
|
|
335
|
+
elif dest and hit[0] != dest:
|
|
336
|
+
problems.append(f"{self.p.rel(f)}:{n.line} {n.tag} DEST={dest} but {tgt} is a {hit[0]}")
|
|
337
|
+
from .edit import schema_check
|
|
338
|
+
if schema_check(f, self.original[f]) is None:
|
|
339
|
+
err = schema_check(f, ft.data)
|
|
340
|
+
if err:
|
|
341
|
+
problems.append(f"{self.p.rel(f)}: schema: {err}")
|
|
342
|
+
return problems
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def arxml_edit(project: Project, ops, dry_run: bool = False) -> dict:
|
|
346
|
+
if isinstance(ops, dict):
|
|
347
|
+
ops = [ops]
|
|
348
|
+
if not isinstance(ops, list) or not ops or not all(isinstance(o, dict) for o in ops):
|
|
349
|
+
return {"ok": False, "error": "ops must be a non-empty list of op objects"}
|
|
350
|
+
ed = ArxmlEditor(project)
|
|
351
|
+
created = []
|
|
352
|
+
try:
|
|
353
|
+
for i, op in enumerate(ops):
|
|
354
|
+
kind = op.get("op")
|
|
355
|
+
try:
|
|
356
|
+
if kind == "add":
|
|
357
|
+
created.append(ed.op_add(op))
|
|
358
|
+
elif kind == "set":
|
|
359
|
+
ed.op_set(op)
|
|
360
|
+
elif kind == "remove":
|
|
361
|
+
ed.op_remove(op)
|
|
362
|
+
else:
|
|
363
|
+
raise ArxmlEditError(f"unknown op {kind!r} (add/set/remove)")
|
|
364
|
+
except ArxmlEditError as e:
|
|
365
|
+
raise ArxmlEditError(f"op {i} ({kind} {op.get('path') or op.get('parent_path')}): {e}") from None
|
|
366
|
+
except ArxmlEditError as e:
|
|
367
|
+
return {"ok": False, "error": str(e), "written": False}
|
|
368
|
+
problems = ed.check()
|
|
369
|
+
changes = []
|
|
370
|
+
for f, ft in ed.files.items():
|
|
371
|
+
if ft.data != ed.original[f]:
|
|
372
|
+
a = ed.original[f].decode("utf-8", "replace").splitlines(keepends=True)
|
|
373
|
+
b = ft.data.decode("utf-8", "replace").splitlines(keepends=True)
|
|
374
|
+
rel = project.rel(f)
|
|
375
|
+
changes.append({"file": rel, "diff": "".join(difflib.unified_diff(a, b, f"a/{rel}", f"b/{rel}", n=1))})
|
|
376
|
+
if problems:
|
|
377
|
+
return {"ok": False, "error": "edit rejected; nothing written", "problems": problems, "changes": changes,
|
|
378
|
+
"written": False}
|
|
379
|
+
if not dry_run:
|
|
380
|
+
for f, ft in ed.files.items():
|
|
381
|
+
if ft.data != ed.original[f]:
|
|
382
|
+
tmp = f + ".arxml-tmp"
|
|
383
|
+
with open(tmp, "wb") as fh:
|
|
384
|
+
fh.write(ft.data)
|
|
385
|
+
os.replace(tmp, f)
|
|
386
|
+
return {"ok": True, "written": not dry_run, "created": [c for c in created if c], "changes": changes,
|
|
387
|
+
"schemaChecked": _ad() is not None}
|