Files

699 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# xdto-info v1.0 — Analyze 1C XDTO package structure (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
XS_NS = "http://www.w3.org/2001/XMLSchema"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-PackagePath", "-Path", required=True)
parser.add_argument("-Package", default="")
parser.add_argument("-Namespace", default="")
parser.add_argument("-Name", default="")
parser.add_argument("-Mode", default="auto", choices=["auto", "used-by"])
parser.add_argument("-Depth", type=int, default=1)
parser.add_argument("-RequiredOnly", action="store_true")
parser.add_argument("-Limit", type=int, default=150)
parser.add_argument("-Offset", type=int, default=0)
parser.add_argument("-OutFile", default="")
args = parser.parse_args()
LIMIT, OFFSET, DEPTH = args.Limit, args.Offset, args.Depth
lines = []
def O(line=""):
lines.append(line)
def flush_output():
text = "\n".join(lines).rstrip()
if args.OutFile:
d = os.path.dirname(args.OutFile)
if d and not os.path.isdir(d):
os.makedirs(d, exist_ok=True)
with open(args.OutFile, "wb") as f:
f.write(b"\xef\xbb\xbf" + (text + "\r\n").encode("utf-8"))
print(f"✓ Записано: {args.OutFile}")
else:
print(text)
def die(msg):
# Отрицательный результат поиска — не исключение: сообщение и код 1,
# без трассировки
print(msg)
sys.exit(1)
def _parse_xml(source, from_string=False):
"""Разбор с узким отступлением для не-URI пространств имён.
Платформа допускает в targetNamespace произвольную строку (в выгрузке БП есть
пакет с кириллическим «ДопФайлУниверсальный»), .NET такое принимает, а libxml2
отвергает. Откатываемся на восстанавливающий разбор ТОЛЬКО на этой ошибке,
иначе по-настоящему битый XML перестал бы отличаться от корректного.
"""
try:
return (etree.fromstring(source) if from_string else etree.parse(source))
except etree.XMLSyntaxError as e:
if "is not a valid URI" not in str(e):
raise
p = etree.XMLParser(recover=True)
return (etree.fromstring(source, p) if from_string else etree.parse(source, p))
def local(el):
return etree.QName(el).localname
# ── resolve path ─────────────────────────────────────────────
package_path = os.path.abspath(args.PackagePath)
if not os.path.exists(package_path):
die(f"Путь не найден: {package_path}")
config_root = None
direct_pkg_dir = None
if os.path.exists(os.path.join(package_path, "Configuration.xml")):
config_root = package_path
elif os.path.basename(package_path.rstrip("\\/")) == "XDTOPackages":
config_root = os.path.dirname(package_path.rstrip("\\/"))
elif os.path.exists(os.path.join(package_path, "Ext", "Package.bin")):
direct_pkg_dir = package_path
config_root = os.path.dirname(os.path.dirname(package_path))
elif os.path.isfile(package_path) and os.path.basename(package_path) == "Package.bin":
direct_pkg_dir = os.path.dirname(os.path.dirname(package_path))
config_root = os.path.dirname(os.path.dirname(direct_pkg_dir))
elif package_path.endswith(".xml"):
stem = os.path.join(os.path.dirname(package_path),
os.path.splitext(os.path.basename(package_path))[0])
if os.path.exists(os.path.join(stem, "Ext", "Package.bin")):
direct_pkg_dir = stem
config_root = os.path.dirname(os.path.dirname(stem))
if not config_root and not direct_pkg_dir:
die(f"Не удалось определить пакет или конфигурацию по пути: {package_path}")
# ── package index ────────────────────────────────────────────
class Pkg:
__slots__ = ("Name", "Dir", "Namespace", "Root", "Imports", "Types", "GlobalProps")
def read_package(pkg_dir):
b = os.path.join(pkg_dir, "Ext", "Package.bin")
if not os.path.exists(b):
return None
try:
root = _parse_xml(b).getroot()
except Exception: # noqa: BLE001
return None
if local(root) != "package":
return None
p = Pkg()
p.Name = os.path.basename(pkg_dir.rstrip("\\/"))
p.Dir = pkg_dir
p.Namespace = root.get("targetNamespace")
p.Root = root
p.Imports = []
p.Types = {}
p.GlobalProps = []
for n in root:
if not isinstance(n.tag, str):
continue
ln = local(n)
if ln == "import":
p.Imports.append(n.get("namespace"))
elif ln in ("objectType", "valueType"):
p.Types[n.get("name")] = n
elif ln == "property":
p.GlobalProps.append(n)
return p
packages = []
by_namespace = {}
if config_root and os.path.isdir(os.path.join(config_root, "XDTOPackages")):
base = os.path.join(config_root, "XDTOPackages")
for dn in sorted(d for d in os.listdir(base) if os.path.isdir(os.path.join(base, d))):
p = read_package(os.path.join(base, dn))
if p:
packages.append(p)
by_namespace.setdefault(p.Namespace, p)
if direct_pkg_dir and not packages:
p = read_package(direct_pkg_dir)
if p:
packages.append(p)
by_namespace[p.Namespace] = p
if not packages:
die(f"Пакеты XDTO не найдены: {package_path}")
# ── type notation: XSD -> 1С ─────────────────────────────────
XS_TO_1C = {
"string": "Строка", "normalizedString": "Строка", "token": "Строка", "NCName": "Строка",
"Name": "Строка", "QName": "Строка", "anyURI": "Строка", "language": "Строка",
"ID": "Строка", "IDREF": "Строка", "NMTOKEN": "Строка",
"decimal": "Число", "integer": "Число", "int": "Число", "long": "Число", "short": "Число",
"byte": "Число", "float": "Число", "double": "Число",
"nonNegativeInteger": "Число", "positiveInteger": "Число", "nonPositiveInteger": "Число",
"negativeInteger": "Число", "unsignedInt": "Число", "unsignedLong": "Число",
"unsignedShort": "Число", "unsignedByte": "Число",
"date": "Дата", "dateTime": "Дата", "time": "Дата",
"boolean": "Булево",
"base64Binary": "ДвоичныеДанные", "hexBinary": "ДвоичныеДанные",
"anyType": "произвольный", "anySimpleType": "произвольный",
}
FACET_NAMES = ["length", "minLength", "maxLength", "totalDigits", "fractionDigits",
"minInclusive", "maxInclusive", "minExclusive", "maxExclusive"]
def split_ref(el, raw):
if not raw:
return None
if raw.startswith("{"):
close = raw.find("}")
if close < 0:
return None
return (raw[1:close], raw[close + 1:])
parts = raw.split(":")
if len(parts) == 2:
return (el.nsmap.get(parts[0]), parts[1])
return (None, parts[0])
def get_facets(t):
res = {}
for f in FACET_NAMES:
v = t.get(f)
if v:
res[f] = v
for c in t:
if isinstance(c.tag, str) and local(c) == "pattern":
res["pattern"] = c.text or ""
break
return res
def get_enumerations(t):
return [(c.text or "") for c in t if isinstance(c.tag, str) and local(c) == "enumeration"]
def find_type(q, pkg):
if not q:
return None
ns, loc = q
target = None
if not ns or (pkg and ns == pkg.Namespace):
target = pkg
elif ns in by_namespace:
target = by_namespace[ns]
if not target or loc not in target.Types:
return None
return (target.Types[loc], target)
def format_ref_name(q, pkg):
if not q:
return ""
ns, loc = q
if ns == XS_NS:
return XS_TO_1C.get(loc, f"xs:{loc}")
return loc
def resolve_scalar(t, pkg, guard=0):
acc = {"Base1C": None, "Facets": {}, "Alias": None, "Enum": [], "Kind": "scalar"}
if guard > 10 or t is None:
return acc
variety = t.get("variety")
if variety == "List":
it = split_ref(t, t.get("itemType"))
acc["Kind"] = "list"
acc["Base1C"] = "список " + (format_ref_name(it, pkg) if it else "значений")
return acc
if variety == "Union" or t.get("memberTypes"):
members = []
for m in (t.get("memberTypes") or "").split():
q = split_ref(t, m)
if q:
members.append(format_ref_name(q, pkg))
for c in t:
if isinstance(c.tag, str) and local(c) == "typeDef":
members.append(resolve_scalar(c, pkg, guard + 1)["Base1C"])
acc["Kind"] = "union"
acc["Base1C"] = "одно из (" + " | ".join(m for m in members if m) + ")"
return acc
acc["Facets"] = get_facets(t)
acc["Enum"] = get_enumerations(t)
base_q = split_ref(t, t.get("base"))
anon_base = next((c for c in t if isinstance(c.tag, str) and local(c) == "typeDef"), None)
if not base_q and anon_base is not None:
inner = resolve_scalar(anon_base, pkg, guard + 1)
acc["Base1C"] = inner["Base1C"]
for k, v in inner["Facets"].items():
acc["Facets"].setdefault(k, v)
if inner["Enum"] and not acc["Enum"]:
acc["Enum"] = inner["Enum"]
return acc
if not base_q:
acc["Base1C"] = "произвольный"
return acc
if base_q[0] == XS_NS:
acc["Base1C"] = XS_TO_1C.get(base_q[1], f"xs:{base_q[1]}")
return acc
target = find_type(base_q, pkg)
if target and local(target[0]) == "valueType":
inner = resolve_scalar(target[0], target[1], guard + 1)
acc["Base1C"] = inner["Base1C"]
for k, v in inner["Facets"].items():
acc["Facets"].setdefault(k, v)
if inner["Enum"] and not acc["Enum"]:
acc["Enum"] = inner["Enum"]
if not acc["Alias"]:
acc["Alias"] = base_q[1]
return acc
acc["Base1C"] = base_q[1]
return acc
def format_scalar(res):
t, f = res["Base1C"], res["Facets"]
if t == "Строка":
if "length" in f:
t = f'Строка({f["length"]})'
elif "maxLength" in f:
t = f'Строка({f["maxLength"]})'
elif t == "Число":
if "totalDigits" in f:
t = f'Число({f["totalDigits"]},{f.get("fractionDigits", "0")})'
return t
def format_notes(res):
notes = []
if res["Alias"]:
notes.append("← " + res["Alias"])
if "pattern" in res["Facets"]:
p = res["Facets"]["pattern"]
if len(p) > 40:
p = p[:40] + "…"
notes.append("шаблон " + p)
for k in ("minInclusive", "maxInclusive", "minExclusive", "maxExclusive"):
if k in res["Facets"]:
notes.append(f'{k} {res["Facets"][k]}')
return notes
# ── property rendering ───────────────────────────────────────
def get_prop_rows(type_el, pkg, depth, indent, seen):
rows = []
for p in type_el:
if not isinstance(p.tag, str) or local(p) != "property":
continue
pname = p.get("name")
if not pname:
ref_q = split_ref(p, p.get("ref"))
pname = ref_q[1] if ref_q else "(без имени)"
lower, upper = p.get("lowerBound"), p.get("upperBound")
flags = []
# В модели умолчание lowerBound = 1; помечаем обязательные, как в meta-info
if lower != "0":
flags.append("обязательный")
if upper == "-1":
flags.append("список")
elif upper and upper != "1":
flags.append("до " + upper)
if p.get("form") == "Text":
flags.append("значение элемента")
notes = []
type_text = ""
children = None
child_pkg = pkg
# Именованный вложенный тип берётся так же, как корневой; окольный путь
# через свойство владельца нужен только анонимному — имени у него нет
obj_name = None
obj_ns = None
anon = next((c for c in p if isinstance(c.tag, str) and local(c) == "typeDef"), None)
if anon is not None:
if anon.get(f"{{{XSI_NS}}}type") == "ObjectType":
type_text = "объект (анонимный)"
obj_name = "(анонимный)"
children = anon # анонимные раскрываем всегда: смотреть отдельно негде
else:
res = resolve_scalar(anon, pkg)
type_text = format_scalar(res)
notes += format_notes(res)
if res["Enum"]:
notes.append("значения: " + ", ".join(res["Enum"][:8]))
else:
q = split_ref(p, p.get("type"))
if not q:
type_text = "произвольный"
elif q[0] == XS_NS:
type_text = XS_TO_1C.get(q[1], f"xs:{q[1]}")
else:
target = find_type(q, pkg)
if not target:
type_text = "объект " + q[1]
notes.append(f"(пакет не найден: {q[0]})")
elif local(target[0]) == "objectType":
type_text = "объект " + q[1]
if target[1].Namespace != pkg.Namespace:
type_text += " · " + target[1].Name
obj_name = q[1]
obj_ns = target[1].Namespace
children = target[0]
child_pkg = target[1]
else:
res = resolve_scalar(target[0], target[1])
type_text = format_scalar(res)
notes.append("← " + q[1])
notes += [n for n in format_notes(res) if not n.startswith("←")]
if res["Enum"]:
notes.append("значения: " + ", ".join(res["Enum"][:8]))
rows.append({"Indent": indent, "Name": pname, "Type": type_text,
"Flags": flags, "Notes": [n for n in notes if n],
"ObjName": obj_name, "ObjNs": obj_ns})
if children is not None:
cname = children.get("name")
key = f"{child_pkg.Namespace}#{cname}"
is_anon = not cname
if not is_anon and key in seen:
rows.append({"Indent": indent + 1, "Name": "(раскрыт выше)", "Type": "",
"Flags": [], "Notes": [], "ObjName": None, "ObjNs": None})
elif is_anon or depth > 1:
next_seen = set(seen)
if not is_anon:
next_seen.add(key)
next_depth = depth if is_anon else depth - 1
rows += get_prop_rows(children, child_pkg, next_depth, indent + 1, next_seen)
return rows
# Оставить только обязательные свойства. Ребёнок необязательного объекта тоже
# уходит: он лежит под необязательной веткой и заполнять его не обязательно.
def select_required(rows):
res = []
cut_from = -1
for r in rows:
if cut_from >= 0 and r["Indent"] > cut_from:
continue
cut_from = -1
if "обязательный" not in r["Flags"]:
cut_from = r["Indent"]
continue
res.append(r)
return res
def write_rows(rows):
if not rows:
O(" (нет свойств)")
return
shown = rows
if OFFSET > 0 or len(rows) > LIMIT:
shown = rows[OFFSET:OFFSET + LIMIT]
w_name = max((len(" " * r["Indent"] + r["Name"]) for r in shown), default=0)
w_type = max((len(r["Type"]) for r in shown), default=0)
for r in shown:
n = " " * r["Indent"] + r["Name"]
line = " " + n.ljust(w_name + 2) + r["Type"].ljust(w_type + 2)
if r["Flags"]:
line += "[" + ", ".join(r["Flags"]) + "] "
if r["Notes"]:
line += ", ".join(r["Notes"])
O(line.rstrip())
if len(rows) > len(shown):
O(f" … показано {len(shown)} из {len(rows)}; листать через -Offset/-Limit")
# ── modes ────────────────────────────────────────────────────
def show_package_list():
O(f"=== Пакеты XDTO: {len(packages)} ===")
O("")
shown = packages
if OFFSET > 0 or len(packages) > LIMIT:
shown = packages[OFFSET:OFFSET + LIMIT]
wn = max((len(p.Name) for p in shown), default=0)
for p in shown:
O(" " + p.Name.ljust(wn + 2) + str(len(p.Types)).rjust(4) + " " + p.Namespace)
if len(packages) > len(shown):
O("")
O(f" … показано {len(shown)} из {len(packages)}; листать через -Offset/-Limit")
O("")
O("Колонки: имя пакета, число типов, namespace.")
O("Следующий шаг: -Package <имя> или -Namespace <URI> — состав пакета; "
"-Name <Тип> — поиск типа по всем пакетам")
def show_package_overview(pkg):
O(f"=== Пакет XDTO: {pkg.Name} ===")
O(f"Namespace: {pkg.Namespace}")
if pkg.Imports:
O("")
O(f"Импорты ({len(pkg.Imports)}):")
for i in pkg.Imports:
dep = by_namespace[i].Name if i in by_namespace else "(пакет не найден)"
O(f" {i}{dep}")
if not pkg.GlobalProps:
O("")
O("Точки входа: нет — пакет не объявляет корневых элементов документа")
else:
O("")
O(f"Точки входа ({len(pkg.GlobalProps)}) — корневые элементы документа:")
for gp in pkg.GlobalProps:
q = split_ref(gp, gp.get("type"))
tn = format_ref_name(q, pkg) if q else "произвольный"
form = " (атрибут)" if gp.get("form") == "Attribute" else ""
O(f' <{gp.get("name")}> → {tn}{form}')
objs = [k for k in sorted(pkg.Types) if local(pkg.Types[k]) == "objectType"]
vals = [k for k in sorted(pkg.Types) if local(pkg.Types[k]) == "valueType"]
if objs:
O("")
O(f"Объектные типы ({len(objs)}):")
for n in objs:
cnt = sum(1 for c in pkg.Types[n] if isinstance(c.tag, str) and local(c) == "property")
base = split_ref(pkg.Types[n], pkg.Types[n].get("base"))
suffix = f" ← {base[1]}" if base else ""
O(" " + n.ljust(40) + f"свойств: {cnt}" + suffix)
if vals:
O("")
O(f"Типы значений ({len(vals)}):")
for n in vals:
res = resolve_scalar(pkg.Types[n], pkg)
line = " " + n.ljust(40) + format_scalar(res)
if res["Enum"]:
line += " значения: " + ", ".join(res["Enum"][:6])
O(line)
O("")
O("Следующий шаг: -Name <Тип> — структура типа для заполнения")
# Легенда едет вместе с выводом, а не живёт в инструкции: показываем только те
# обозначения, которые реально встретились, иначе она сама становится шумом.
def write_legend(rows):
text = " ".join(r["Type"] + " " + ",".join(r["Flags"]) + " " + ",".join(r["Notes"]) for r in rows)
items = []
if "объект " in text:
items.append("объект X — присвоить вложенный объект XDTO, состав раскрывает -Depth")
if "←" in text:
items.append("← Имя — исходный тип из схемы, слева от стрелки развёрнутое значение")
if "список" in text:
items.append("список — коллекция, заполняется через .Добавить()")
if re.search(r"до \d", text):
items.append("до N — коллекция с ограничением сверху")
if "значение элемента" in text:
items.append("значение элемента — собственное значение узла XML")
if "·" in text:
items.append("· Пакет — тип объявлен в другом пакете")
if not items:
return
O("")
O("Обозначения:")
for i in items:
O(" " + i)
def show_type(pkg, type_name):
el = pkg.Types[type_name]
if local(el) == "valueType":
res = resolve_scalar(el, pkg)
O(f"=== Тип значения XDTO: {type_name} ===")
O(f"Пакет: {pkg.Name} · {pkg.Namespace}")
O("")
O("Значение: " + format_scalar(res))
for n in format_notes(res):
O(" " + n)
if res["Enum"]:
O("")
O(f'Допустимые значения ({len(res["Enum"])}):')
for v in res["Enum"]:
O(" " + v)
O("")
O("Создание:")
# Создать(<Тип>, <Значение>) принимает именно ТипЗначенияXDTO —
# для объектного типа эта форма неприменима
O(f' Значение = ФабрикаXDTO.Создать(ФабрикаXDTO.Тип("{pkg.Namespace}", "{type_name}"), Значение);')
return
hdr = f"=== Тип XDTO: {type_name} ==="
if DEPTH > 1:
hdr += f" (глубина {DEPTH})"
O(hdr)
O(f"Пакет: {pkg.Name} · {pkg.Namespace}")
base = split_ref(el, el.get("base"))
if base:
O("Наследует: " + base[1])
if el.get("abstract") == "true":
O("Абстрактный — создаётся только тип-наследник")
if el.get("open") == "true":
O("Открытый — допускает произвольные элементы и атрибуты")
O("")
seen = {f"{pkg.Namespace}#{type_name}"}
rows = get_prop_rows(el, pkg, DEPTH, 0, seen)
own = [r for r in rows if r["Indent"] == 0]
if args.RequiredOnly:
total = len(rows)
rows = select_required(rows)
own_req = [r for r in rows if r["Indent"] == 0]
# Фильтр обязан сообщать о себе: иначе список читается как полный
O(f"Свойства: обязательных {len(own_req)} из {len(own)} "
f"(-RequiredOnly; скрыто строк: {total - len(rows)})")
else:
O(f"Свойства ({len(own)}):")
write_rows(rows)
write_legend(rows)
O("")
O("Создание:")
O(f' Тип = ФабрикаXDTO.Тип("{pkg.Namespace}", "{type_name}");')
O(" Объект = ФабрикаXDTO.Создать(Тип);")
# Рецепты для вложенных и анонимных типов: имени у анонимного нет, через
# ФабрикаXDTO.Тип(ns, имя) его не получить — только от свойства владельца
named = next((r for r in rows if r.get("ObjName") and r.get("ObjName") != "(анонимный)"), None)
if named:
O(" // вложенный именованный тип — так же, как корневой:")
O(f' {named["Name"]} = ФабрикаXDTO.Создать(ФабрикаXDTO.Тип("{named["ObjNs"]}", "{named["ObjName"]}"));')
anon_row = next((r for r in rows if r.get("ObjName") == "(анонимный)"), None)
if anon_row:
O(" // у анонимного типа нет имени — только через свойство владельца:")
O(f' {anon_row["Name"]} = ФабрикаXDTO.Создать(Тип.Свойства.Получить("{anon_row["Name"]}").Тип);')
text_row = next((r for r in rows if "значение элемента" in r["Flags"]), None)
if text_row:
O(f' // собственное значение узла лежит в свойстве {text_row["Name"]}')
def show_used_by(type_name, owner_pkg):
O(f"=== Ссылки на тип: {type_name} ===")
if owner_pkg:
O(f"Объявлен в: {owner_pkg.Name} · {owner_pkg.Namespace}")
O("")
hits = []
for p in packages:
for node in p.Root.iter():
if not isinstance(node.tag, str):
continue
for a in ("type", "base", "itemType", "memberTypes"):
raw = node.get(a)
if not raw:
continue
for one in raw.split():
q = split_ref(node, one)
if not q or q[1] != type_name:
continue
if owner_pkg and q[0] and q[0] != owner_pkg.Namespace:
continue
owner = node
while owner is not None and local(owner) not in ("objectType", "valueType"):
owner = owner.getparent()
where = owner.get("name") if (owner is not None and owner.get("name")) else "(верхний уровень)"
what = node.get("name") or local(node)
hits.append(f" {p.Name}.{where}.{what} ({a})")
if not hits:
O(" Ссылок не найдено")
return
O(f"Найдено ({len(hits)}):")
for h in sorted(set(hits)):
O(h)
# ── dispatch ─────────────────────────────────────────────────
selected = None
if direct_pkg_dir:
leaf = os.path.basename(direct_pkg_dir.rstrip("\\/"))
selected = next((p for p in packages if p.Name == leaf), None)
if not selected and args.Namespace:
selected = next((p for p in packages if p.Namespace == args.Namespace), None)
if not selected:
die(f'Пакет с namespace "{args.Namespace}" не найден. '
"Список: -PackagePath <корень> без параметров")
if not selected and args.Package:
selected = next((p for p in packages if p.Name == args.Package), None)
if not selected:
die(f'Пакет "{args.Package}" не найден. Список: -PackagePath <корень> без параметров')
if args.Mode == "used-by":
if not args.Name:
die("Режим used-by требует -Name <Тип>")
owner = selected or next((p for p in packages if args.Name in p.Types), None)
show_used_by(args.Name, owner)
flush_output()
sys.exit(0)
if args.Name:
if not selected:
# Тип известен, пакет — нет: ищем по всей конфигурации
found = [p for p in packages if args.Name in p.Types]
if not found:
die(f'Тип "{args.Name}" не найден ни в одном пакете конфигурации')
if len(found) > 1:
O(f'=== Тип "{args.Name}" найден в нескольких пакетах ({len(found)}) ===')
O("Уточните через -Namespace или -Package:")
O("")
for f in found:
O(f" {f.Name} · {f.Namespace}")
flush_output()
sys.exit(0)
selected = found[0]
if args.Name not in selected.Types:
die(f'В пакете {selected.Name} нет типа "{args.Name}". Список типов: тот же вызов без -Name')
show_type(selected, args.Name)
flush_output()
sys.exit(0)
if selected:
show_package_overview(selected)
else:
show_package_list()
flush_output()
sys.exit(0)