mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 05:44:14 +02:00
qet-mcp: qet_edit names a folio by its uuid
qet_edit takes a folio as its index, and the index shifts when an earlier op in the same run adds, inserts or removes a folio: remove folio 0, then edit "folio 2", and the edit lands on the folio that was 3. Every other item qet_edit addresses (elements, texts, shapes, pictures, tables, symbol text fields) can already be named by uuid; folios could not. - "folio" and "to_folio" take the folio's uuid as well as its index, turned into the current index at run time by qet.folioIndex(). The lookup is required only when a uuid is used, so an index-only edit still runs on a build without it. - qet_project_info lists each folio's uuid. A folio saved without one (132 of the 133 in the shipped examples) shows it empty until the project is saved once, and an empty "folio" says so. Tests: the generated script for a uuid folio, on its own and inside a table lookup and link_elements' to_folio; and a real run that removes folio 0 and then retitles the old folio 2 by uuid, on a file saved without folio uuids. The run fails against a build without qet.folioIndex(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,7 @@ here read the model.
|
||||
|
||||
| Tool | What it answers |
|
||||
|---|---|
|
||||
| `qet_project_info` | title, format version, folios, element and conductor counts |
|
||||
| `qet_project_info` | title, format version, folios with their uuids, element and conductor counts |
|
||||
| `qet_elements` | placed elements: uuid, type, position, label, information bag |
|
||||
| `qet_conductors` | conductors and their documentation fields; filter by attribute |
|
||||
| `qet_items` | free texts, shapes, pictures, tables and symbol text fields, each with its uuid |
|
||||
@@ -290,6 +290,12 @@ Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
`index` also takes that uuid, which does not shift the way an index
|
||||
does. `qet_element_build` gives every part of a symbol a uuid as well,
|
||||
returned in `part_uuids`; `qet_element_info` lists them in `part_list`.
|
||||
- **A folio can be named by its uuid** wherever an op takes `folio` or
|
||||
`to_folio`; `qet_project_info` lists each folio's. It still names the same
|
||||
folio after an earlier op in the run adds, inserts or removes one, where
|
||||
an index would shift. A folio saved without a uuid shows it empty:
|
||||
QElectroTech gives it one on load and writes it on the next save, so it
|
||||
appears after a first `qet_edit`. Needs `qet.folioIndex()` in the build.
|
||||
- **`qet_export` isolates its launch.** SingleApplication keys its socket
|
||||
on `applicationFilePath()`, so a second launch of the same binary path
|
||||
forwards its request to an already-running instance and returns *that*
|
||||
|
||||
+21
-5
@@ -250,6 +250,7 @@ def tool_project_info(path: str) -> dict:
|
||||
for i, d in _folios(root):
|
||||
folios.append({
|
||||
"index": i,
|
||||
"uuid": d.get("uuid", ""),
|
||||
"title": d.get("title", ""),
|
||||
"elements": sum(1 for _ in d.iter("element")),
|
||||
"conductors": len(_wires(d)),
|
||||
@@ -1480,9 +1481,19 @@ def _build_script(operations: list, output: str) -> str:
|
||||
f"its uuid, or a \"$name\" reference, got {value!r}")
|
||||
return _js(value)
|
||||
if kind == "folio":
|
||||
# A uuid names the folio for good; it is turned into the index
|
||||
# the call takes at run time, since adding or removing a folio
|
||||
# shifts every index after it.
|
||||
if isinstance(value, str) and _UUID_RE.fullmatch(value):
|
||||
uuid_methods.add("folioIndex")
|
||||
return f"qet.folioIndex({_js(value)})"
|
||||
if value == "":
|
||||
raise ValueError(f"operation {op_index}: {key!r} is empty -- a folio saved "
|
||||
f"without a uuid has none in the file until the project is "
|
||||
f"saved once; give its index instead")
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise ValueError(f"operation {op_index}: {key!r} must be a folio index "
|
||||
f"or a \"$name\" reference, got {value!r}")
|
||||
raise ValueError(f"operation {op_index}: {key!r} must be a folio index, "
|
||||
f"its uuid, or a \"$name\" reference, got {value!r}")
|
||||
return _js(value)
|
||||
if kind == "bool":
|
||||
if not isinstance(value, bool):
|
||||
@@ -2308,8 +2319,10 @@ def tool_edit(binary: str, project: str, operations: list, output: str,
|
||||
TOOLS = [
|
||||
{
|
||||
"name": "qet_project_info",
|
||||
"description": "Summarise a .qet project: title, format version, folios, "
|
||||
"and element/conductor counts per folio. Reads the file "
|
||||
"description": "Summarise a .qet project: title, format version, folios "
|
||||
"with their uuids, and element/conductor counts per folio. A "
|
||||
"folio saved without a uuid shows it empty; QElectroTech gives "
|
||||
"it one on load and writes it on the next save. Reads the file "
|
||||
"directly; does not launch QElectroTech.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
@@ -2487,7 +2500,10 @@ TOOLS = [
|
||||
"add_element gets wired by add_conductor, and how a folio made "
|
||||
"by add_folio is addressed. A \"folio\" given as a number is "
|
||||
"an index counted from 0: the folio qet_elements and "
|
||||
"qet_project_info call 1 is \"folio\": 0 here. Terminals are numbered by their "
|
||||
"qet_project_info call 1 is \"folio\": 0 here. A folio can "
|
||||
"be given as its uuid instead (qet_project_info lists them), "
|
||||
"which still names the same folio after an earlier op adds or "
|
||||
"removes one. Terminals are numbered by their "
|
||||
"index in the element definition; qet_element_info lists them. "
|
||||
"set_conductor addresses a conductor as the one on a given "
|
||||
"terminal and applies the change to its whole electrical "
|
||||
|
||||
@@ -194,6 +194,26 @@ class EditValidation(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "index or its uuid"):
|
||||
self.build([op])
|
||||
|
||||
def test_folio_by_uuid(self):
|
||||
"""A folio named by uuid is looked up at run time, wherever an op
|
||||
takes a folio; the lookup is required only then."""
|
||||
U = "{11111111-2222-4333-8444-555555555555}"
|
||||
E = "{aaaaaaaa-0000-4000-8000-000000000001}"
|
||||
s = self.build([{"op": "set_folio", "folio": U, "property": "author", "value": "a"}])
|
||||
self.assertIn(f'qet.setFolioProperty(qet.folioIndex("{U}"), "author", "a")', s)
|
||||
self.assertIn('"folioIndex"', s)
|
||||
# the item lookups take the resolved folio too
|
||||
s = self.build([{"op": "delete_table", "folio": U, "table": E}])
|
||||
self.assertIn(f'qet.deleteTable(qet.folioIndex("{U}"), qet.tableIndex(qet.folioIndex("{U}"), "{E}"))', s)
|
||||
s = self.build([{"op": "link_elements", "folio": 0, "element": E, "to_folio": U, "to": E}])
|
||||
self.assertIn(f'qet.linkElements(0, "{E}", qet.folioIndex("{U}"), "{E}")', s)
|
||||
self.assertNotIn('"folioIndex"', self.build(
|
||||
[{"op": "set_folio", "folio": 0, "property": "author", "value": "a"}]))
|
||||
with self.assertRaisesRegex(ValueError, "saved once"):
|
||||
self.build([{"op": "set_folio", "folio": "", "property": "author", "value": "a"}])
|
||||
with self.assertRaisesRegex(ValueError, "folio index, its uuid"):
|
||||
self.build([{"op": "set_folio", "folio": "first", "property": "author", "value": "a"}])
|
||||
|
||||
def test_every_op_generates_a_script(self):
|
||||
# one minimal valid instance of every op
|
||||
f = {"op": "add_folio", "id": "f"}
|
||||
@@ -783,8 +803,8 @@ class ReadToolContracts(unittest.TestCase):
|
||||
self.assertEqual(m.tool_project_info(self.qet), {
|
||||
"file": self.qet, "title": "T", "version": "0.100",
|
||||
"folio_count": 2, "element_count": 4, "conductor_count": 3,
|
||||
"folios": [{"index": 1, "title": "One", "elements": 3, "conductors": 2},
|
||||
{"index": 2, "title": "Two", "elements": 1, "conductors": 1}]})
|
||||
"folios": [{"index": 1, "uuid": "", "title": "One", "elements": 3, "conductors": 2},
|
||||
{"index": 2, "uuid": "", "title": "Two", "elements": 1, "conductors": 1}]})
|
||||
|
||||
def test_elements_rows_exact(self):
|
||||
r = m.tool_elements(self.qet)
|
||||
@@ -3588,6 +3608,31 @@ class UuidIndexLookups(unittest.TestCase):
|
||||
self.assertEqual([(t.get("uuid"), float(t.get("x")), float(t.get("y"))) for t in left],
|
||||
[(twin.get("uuid"), 120.0, 340.0)])
|
||||
|
||||
def test_qet_edit_follows_a_folio_by_uuid_across_a_removal(self):
|
||||
"""Remove folio 0, then retitle what was folio 2 by its uuid. By index
|
||||
the second op would retitle the wrong folio. The file is saved
|
||||
without folio uuids, so qet_project_info shows them only once a
|
||||
first qet_edit has saved it."""
|
||||
src = Path(EXAMPLES) / "tableau_domestique.qet"
|
||||
self.assertEqual({f["uuid"] for f in m.tool_project_info(str(src))["folios"]}, {""})
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
once, out = Path(tmp) / "once.qet", Path(tmp) / "out.qet"
|
||||
r = m.tool_edit(BINARY, str(src), [{"op": "set_folio", "folio": 0, "property": "author",
|
||||
"value": "x"}], str(once), elements_dir=ELEMENTS or None)
|
||||
self.assertTrue(r["ok"], r.get("hint"))
|
||||
before = m.tool_project_info(str(once))["folios"]
|
||||
self.assertTrue(all(m._UUID_RE.fullmatch(f["uuid"]) for f in before))
|
||||
third = before[2]
|
||||
r = m.tool_edit(BINARY, str(once), [
|
||||
{"op": "remove_folio", "folio": before[0]["uuid"]},
|
||||
{"op": "set_folio", "folio": third["uuid"], "property": "title", "value": "moved"}],
|
||||
str(out), elements_dir=ELEMENTS or None)
|
||||
self.assertTrue(r["ok"], r.get("hint"))
|
||||
after = m.tool_project_info(str(out))["folios"]
|
||||
self.assertEqual([f["uuid"] for f in after], [f["uuid"] for f in before[1:]])
|
||||
self.assertEqual([f["title"] for f in after],
|
||||
[f["title"] for f in before[1:2]] + ["moved"] + [f["title"] for f in before[3:]])
|
||||
|
||||
def test_qet_edit_edits_one_copys_field_by_uuid(self):
|
||||
"""Two copies of a symbol share a field uuid; addressing it with the
|
||||
symbol changes that copy's field only."""
|
||||
|
||||
Reference in New Issue
Block a user