mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-28 04:55:53 +03:00
fix(meta-edit): отказ вместо предупреждения, когда операция не выполнена
WARN с кодом 0 означал «не смог»: в пакетном прогоне это выглядело как успех. Теперь предупреждение остаётся только там, где нужное уже есть (элемент уже добавлен, удалять нечего, позиция after/before не найдена — добавлено в конец). Остальное — ошибка в stderr и код 1: неизвестный ключ или операция, неверный формат, недопустимый у типа ребёнок, изменение несуществующего элемента, формы и макеты (их делают form-add/template-add). Отказ атомарен: побочные файлы (таблица внешнего источника, модуль команды, предопределённые) пишутся после основного XML, так что отказ посреди определения не оставляет на диске ни одного изменения. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
51bcc503b0
commit
cfd2d1be50
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-edit v1.55 — Edit existing 1C metadata object XML
|
||||
# meta-edit v1.56 — Edit existing 1C metadata object XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -376,6 +376,15 @@ def die(msg):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Побочные файлы (таблица внешнего источника, модуль команды, предопределённые) пишутся после
|
||||
# основного XML: отказ посреди определения не оставляет на диске ни одного изменения.
|
||||
pending_writes = {}
|
||||
|
||||
|
||||
def exists_pending_or_file(path):
|
||||
return path in pending_writes or os.path.exists(path)
|
||||
|
||||
|
||||
def localname(el):
|
||||
return etree.QName(el.tag).localname
|
||||
|
||||
@@ -2087,8 +2096,7 @@ def convert_inline_to_definition(operation, value):
|
||||
for item in items:
|
||||
dot_idx = item.find(".")
|
||||
if dot_idx <= 0:
|
||||
warn(f"Invalid ts-attribute format (expected TSName.AttrDef): {item}")
|
||||
continue
|
||||
die(f"Invalid ts-attribute format (expected TSName.AttrDef): {item}")
|
||||
ts_name = item[:dot_idx].strip()
|
||||
rest = item[dot_idx + 1:].strip()
|
||||
if ts_name not in ts_groups:
|
||||
@@ -2109,8 +2117,7 @@ def convert_inline_to_definition(operation, value):
|
||||
for elem_def in ts_groups[ts_name]:
|
||||
colon_idx = elem_def.find(":")
|
||||
if colon_idx <= 0:
|
||||
warn(f"Invalid modify format (expected Name: key=val): {elem_def}")
|
||||
continue
|
||||
die(f"Invalid modify format (expected Name: key=val): {elem_def}")
|
||||
elem_name = elem_def[:colon_idx].strip()
|
||||
changes_part = elem_def[colon_idx + 1:].strip()
|
||||
changes_obj = {}
|
||||
@@ -2198,7 +2205,7 @@ def convert_inline_to_definition(operation, value):
|
||||
v = kv[eq_idx + 1:].strip()
|
||||
props_obj[k] = v
|
||||
else:
|
||||
warn(f"Invalid property format (expected Key=Value): {kv}")
|
||||
die(f"Invalid property format (expected Key=Value): {kv}")
|
||||
definition["modify"] = {"properties": props_obj}
|
||||
else:
|
||||
# "ElementName: key=val, key=val ;; Element2: key=val"
|
||||
@@ -2207,8 +2214,7 @@ def convert_inline_to_definition(operation, value):
|
||||
for elem_def in elem_defs:
|
||||
colon_idx = elem_def.find(":")
|
||||
if colon_idx <= 0:
|
||||
warn(f"Invalid modify format (expected Name: key=val): {elem_def}")
|
||||
continue
|
||||
die(f"Invalid modify format (expected Name: key=val): {elem_def}")
|
||||
elem_name = elem_def[:colon_idx].strip()
|
||||
changes_part = elem_def[colon_idx + 1:].strip()
|
||||
changes_obj = {}
|
||||
@@ -2293,21 +2299,18 @@ def process_add(add_def):
|
||||
child_type = resolve_child_type_key(raw_key)
|
||||
|
||||
if not child_type:
|
||||
warn(f"Unknown add child type: {raw_key}")
|
||||
continue
|
||||
die(f"Unknown add child type: {raw_key}")
|
||||
|
||||
# Validate allowed. Проверяем НАЛИЧИЕ ключа, а не истинность списка: пустой список
|
||||
# (объект без допустимых детей) трактовался как «ограничений нет», и чужой ребёнок
|
||||
# молча записывался в объект.
|
||||
if obj_type in valid_child_types:
|
||||
if child_type not in valid_child_types[obj_type]:
|
||||
warn(f"{child_type} not allowed for {obj_type}, skipping")
|
||||
continue
|
||||
die(f"{child_type} not allowed for {obj_type}")
|
||||
|
||||
xml_tag = child_type_to_xml_tag.get(child_type)
|
||||
if not xml_tag:
|
||||
warn(f"No XML tag mapping for {child_type}")
|
||||
continue
|
||||
die(f"No XML tag mapping for {child_type}")
|
||||
|
||||
ensure_child_objects_open()
|
||||
indent = get_child_indent(child_objects_el)
|
||||
@@ -2344,13 +2347,12 @@ def process_add(add_def):
|
||||
bad_key = next((k for k in ('characteristics', 'defaultObjectForm', 'defaultRecordForm',
|
||||
'defaultListForm', 'defaultChoiceForm') if tv.get(k)), None)
|
||||
if bad_key:
|
||||
warn(f"Ключ '{bad_key}' не поддержан при добавлении таблицы: форму назначает навык form-add, характеристики — навык meta-compile. Таблица '{tbl_name}' пропущена.")
|
||||
continue
|
||||
die(f"Ключ '{bad_key}' не поддержан при добавлении таблицы: форму назначает навык form-add, характеристики — навык meta-compile.")
|
||||
if tbl_name in existing_names:
|
||||
warn(f"Table '{tbl_name}' already exists, skipping")
|
||||
continue
|
||||
table_path = os.path.join(tables_dir, f"{tbl_name}.xml")
|
||||
if os.path.exists(table_path):
|
||||
if exists_pending_or_file(table_path):
|
||||
warn(f"\u0424\u0430\u0439\u043b \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442: {table_path} \u2014 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u044e")
|
||||
continue
|
||||
field_parts = []
|
||||
@@ -2358,9 +2360,7 @@ def process_add(add_def):
|
||||
field_parts.append(build_attribute_fragment(parse_attribute_shorthand(f), "eds-field", "\t\t\t", "Field"))
|
||||
fields_xml = "\r\n".join(field_parts)
|
||||
table_xml = build_eds_table_xml(obj_name, tbl_name, entry, fields_xml, '', '')
|
||||
os.makedirs(tables_dir, exist_ok=True)
|
||||
with open(table_path, "w", encoding="utf-8-sig", newline="") as fh:
|
||||
fh.write(table_xml.rstrip("\r\n"))
|
||||
pending_writes[table_path] = table_xml.rstrip("\r\n")
|
||||
nodes = import_fragment(f"{indent}<Table>{esc_xml_text(tbl_name)}</Table>")
|
||||
ref_node = find_insertion_point("Table", {"name": tbl_name})
|
||||
for node in nodes:
|
||||
@@ -2498,10 +2498,10 @@ def process_add(add_def):
|
||||
# эту ответственность здесь нельзя: получится висячая регистрация без файла.
|
||||
skill_name = "form-add" if child_type == "forms" else "template-add"
|
||||
what_name = "Форму" if child_type == "forms" else "Макет"
|
||||
warn(
|
||||
die(
|
||||
f"{what_name} добавляет навык {skill_name} "
|
||||
"(он создаёт и файл, и запись в ChildObjects). "
|
||||
"meta-edit этого не делает — операция пропущена."
|
||||
"meta-edit этого не делает."
|
||||
)
|
||||
|
||||
elif child_type == "commands":
|
||||
@@ -2517,9 +2517,7 @@ def process_add(add_def):
|
||||
# он есть у всех команд без исключения. Пишем ту же заготовку, что и meta-compile.
|
||||
cmd_ext_dir = os.path.join(os.path.dirname(resolved_path), obj_name, "Commands", item_name, "Ext")
|
||||
cmd_mod_path = os.path.join(cmd_ext_dir, "CommandModule.bsl")
|
||||
os.makedirs(cmd_ext_dir, exist_ok=True)
|
||||
with open(cmd_mod_path, "w", encoding="utf-8-sig", newline="") as fh:
|
||||
fh.write("&НаКлиенте\r\nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)\r\n\r\n\t// Вставьте обработчик команды.\r\n\r\nКонецПроцедуры\r\n")
|
||||
pending_writes[cmd_mod_path] = ("&НаКлиенте\r\nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)\r\n\r\n\t// Вставьте обработчик команды.\r\n\r\nКонецПроцедуры\r\n")
|
||||
fragment_xml = build_command_fragment(item_name, indent)
|
||||
nodes = import_fragment(fragment_xml)
|
||||
ref_node = find_insertion_point("Command", {"after": "", "before": ""})
|
||||
@@ -2541,21 +2539,18 @@ def process_remove(remove_def):
|
||||
child_type = resolve_child_type_key(raw_key)
|
||||
|
||||
if not child_type:
|
||||
warn(f"Unknown remove child type: {raw_key}")
|
||||
continue
|
||||
die(f"Unknown remove child type: {raw_key}")
|
||||
if child_type == "properties":
|
||||
warn("Cannot remove properties -- use modify instead")
|
||||
continue
|
||||
die("Cannot remove properties -- use modify instead")
|
||||
if child_type in ("forms", "templates"):
|
||||
# Снять регистрацию мало — надо удалить и файлы; это делают form-remove / template-remove.
|
||||
skill_name = "form-remove" if child_type == "forms" else "template-remove"
|
||||
what_name = "Форму" if child_type == "forms" else "Макет"
|
||||
warn(
|
||||
die(
|
||||
f"{what_name} удаляет навык {skill_name} "
|
||||
"(он убирает и файлы, и запись в ChildObjects). "
|
||||
"meta-edit этого не делает — операция пропущена."
|
||||
"meta-edit этого не делает."
|
||||
)
|
||||
continue
|
||||
|
||||
xml_tag = child_type_to_xml_tag.get(child_type)
|
||||
if not xml_tag or child_objects_el is None:
|
||||
@@ -2602,8 +2597,7 @@ def modify_properties(props_def):
|
||||
insert_property_in_order(properties_el, new_nodes[0], None, prop_name)
|
||||
prop_el = new_nodes[0]
|
||||
else:
|
||||
warn(f"Property '{prop_name}': could not create element")
|
||||
continue
|
||||
die(f"Property '{prop_name}': could not create element")
|
||||
|
||||
# Complex property: Owners, RegisterRecords, BasedOn, InputByString
|
||||
if prop_name in complex_property_map:
|
||||
@@ -2659,14 +2653,12 @@ def modify_child_elements(modify_def, child_type):
|
||||
|
||||
xml_tag = child_type_to_xml_tag.get(child_type)
|
||||
if not xml_tag or child_objects_el is None:
|
||||
warn(f"No ChildObjects or unknown tag for {child_type}")
|
||||
return
|
||||
die(f"No ChildObjects or unknown tag for {child_type}")
|
||||
|
||||
for elem_name, changes in modify_def.items():
|
||||
el = find_element_by_name(child_objects_el, xml_tag, elem_name)
|
||||
if el is None:
|
||||
warn(f"{xml_tag} '{elem_name}' not found for modify")
|
||||
continue
|
||||
die(f"{xml_tag} '{elem_name}' not found for modify")
|
||||
|
||||
# Find Properties inside the element
|
||||
props_el = None
|
||||
@@ -2675,8 +2667,7 @@ def modify_child_elements(modify_def, child_type):
|
||||
props_el = gc
|
||||
break
|
||||
if props_el is None:
|
||||
warn(f"{xml_tag} '{elem_name}': no Properties element found")
|
||||
continue
|
||||
die(f"{xml_tag} '{elem_name}': no Properties element found")
|
||||
|
||||
for change_prop, change_value in changes.items():
|
||||
# TS child attribute operations (add/remove/modify attrs inside a TabularSection)
|
||||
@@ -2690,8 +2681,7 @@ def modify_child_elements(modify_def, child_type):
|
||||
|
||||
if change_prop == "add":
|
||||
if ts_child_obj_el is None:
|
||||
warn(f"TS '{elem_name}' has no ChildObjects element, cannot add attributes")
|
||||
continue
|
||||
die(f"TS '{elem_name}' has no ChildObjects element, cannot add attributes")
|
||||
# Ensure ChildObjects is open (not self-closing empty)
|
||||
has_ts_child_elements = any(True for _ in ts_child_obj_el)
|
||||
if not has_ts_child_elements:
|
||||
@@ -2733,8 +2723,7 @@ def modify_child_elements(modify_def, child_type):
|
||||
|
||||
elif change_prop == "modify":
|
||||
if ts_child_obj_el is None:
|
||||
warn(f"TS '{elem_name}' has no ChildObjects, cannot modify attributes")
|
||||
continue
|
||||
die(f"TS '{elem_name}' has no ChildObjects, cannot modify attributes")
|
||||
# Temporarily swap childObjectsEl and recurse
|
||||
saved_child_obj_el = child_objects_el
|
||||
child_objects_el = ts_child_obj_el
|
||||
@@ -2932,8 +2921,7 @@ def process_modify(modify_def):
|
||||
child_type = resolve_child_type_key(raw_key)
|
||||
|
||||
if not child_type:
|
||||
warn(f"Unknown modify child type: {raw_key}")
|
||||
continue
|
||||
die(f"Unknown modify child type: {raw_key}")
|
||||
|
||||
if child_type == "properties":
|
||||
modify_properties(value)
|
||||
@@ -3553,8 +3541,7 @@ def add_complex_property_item(property_name, values):
|
||||
|
||||
map_entry = complex_property_map.get(property_name)
|
||||
if not map_entry:
|
||||
warn(f"Unknown complex property: {property_name}")
|
||||
return
|
||||
die(f"Unknown complex property: {property_name}")
|
||||
if map_entry.get("expand"):
|
||||
values = [expand_data_path(str(v)) for v in values]
|
||||
if map_entry.get("mdref"):
|
||||
@@ -3628,8 +3615,7 @@ def set_complex_property(property_name, values):
|
||||
|
||||
map_entry = complex_property_map.get(property_name)
|
||||
if not map_entry:
|
||||
warn(f"Unknown complex property: {property_name}")
|
||||
return
|
||||
die(f"Unknown complex property: {property_name}")
|
||||
if map_entry.get("expand"):
|
||||
values = [expand_data_path(str(v)) for v in values]
|
||||
if map_entry.get("mdref"):
|
||||
@@ -3824,14 +3810,15 @@ def add_predefined_items(items):
|
||||
path = get_predefined_path()
|
||||
item_list = items if isinstance(items, list) else [items]
|
||||
items_xml = ''.join(build_predef_item_xml('\t', it, code_type) for it in item_list)
|
||||
if os.path.exists(path):
|
||||
if path in pending_writes:
|
||||
text = pending_writes[path].replace('</PredefinedData>', items_xml + '</PredefinedData>')
|
||||
elif os.path.exists(path):
|
||||
# newline='' => без трансляции переводов строк: иначе CRLF молча схлопнется
|
||||
# в LF при чтении и файл будет переписан в LF (#44/#46/#47).
|
||||
with open(path, 'r', encoding='utf-8-sig', newline='') as f:
|
||||
text = f.read()
|
||||
text = text.replace('</PredefinedData>', items_xml + '</PredefinedData>')
|
||||
else:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
hdr = ('<?xml version="1.0" encoding="UTF-8"?>\r\n<PredefinedData xmlns="http://v8.1c.ru/8.3/xcf/predef" '
|
||||
'xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
||||
'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
||||
@@ -3839,10 +3826,7 @@ def add_predefined_items(items):
|
||||
text = hdr + items_xml + '</PredefinedData>'
|
||||
# Без перевода строки в конце — канон #57. Срезаем в ОБЕИХ ветках: файл, созданный
|
||||
# прежней версией навыка, мог унести хвост, а PS-порт срезает безусловно.
|
||||
text = text.rstrip('\r\n')
|
||||
with open(path, 'wb') as f:
|
||||
f.write(b'\xef\xbb\xbf')
|
||||
f.write(text.encode('utf-8'))
|
||||
pending_writes[path] = text.rstrip('\r\n')
|
||||
info(f"Added {len(item_list)} predefined item(s) -> {path}")
|
||||
add_count += len(item_list)
|
||||
|
||||
@@ -3996,8 +3980,7 @@ def main():
|
||||
continue
|
||||
op_key = resolve_operation_key(prop_name)
|
||||
if not op_key:
|
||||
warn(f"Unknown operation: {prop_name}")
|
||||
continue
|
||||
die(f"Unknown operation: {prop_name}")
|
||||
|
||||
if op_key == "add":
|
||||
process_add(prop_value)
|
||||
@@ -4010,6 +3993,12 @@ def main():
|
||||
save_xml(xml_tree, resolved_path)
|
||||
info(f"Saved: {resolved_path}")
|
||||
|
||||
for pw_path, pw_text in pending_writes.items():
|
||||
os.makedirs(os.path.dirname(pw_path), exist_ok=True)
|
||||
with open(pw_path, "wb") as fh:
|
||||
fh.write(b"\xef\xbb\xbf")
|
||||
fh.write(pw_text.encode("utf-8"))
|
||||
|
||||
# --- Auto-validate ---
|
||||
if not args.NoValidate:
|
||||
# Внешняя обработка/отчёт — автономный объект, meta-validate его не знает (#108).
|
||||
|
||||
Reference in New Issue
Block a user