197 lines
6.1 KiB
Python
197 lines
6.1 KiB
Python
"""Component palette: the kits installed in Sedona home, and the components inside each.
|
|
|
|
Deliberately sourced from Sedona home rather than the open file's <schema>, so the
|
|
palette shows everything installed and not just the 17 kits this app happens to use.
|
|
|
|
Version resolution matters here. A kit directory can hold many manifests — ontrolControl
|
|
ships 22 — and the manifest filename carries only a checksum, which sorts arbitrarily.
|
|
The real version number lives in the .kit filename under kits/, as
|
|
'<kit>-<checksum>-<version>.kit', so that is what is used to decide which manifest is
|
|
newest.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import app_state
|
|
|
|
_versions_cache = {} # kit name -> {checksum: version string}
|
|
_manifest_cache = {} # kit name -> (version string, {type name: element})
|
|
_component_cache = {} # 'kit::Type' -> bool
|
|
|
|
|
|
def _manifests_dir():
|
|
return os.path.join(app_state.SEDONA_HOME, "manifests")
|
|
|
|
|
|
def _kits_dir():
|
|
return os.path.join(app_state.SEDONA_HOME, "kits")
|
|
|
|
|
|
def list_kits():
|
|
"""Kit names installed in Sedona home, alphabetically."""
|
|
directory = _manifests_dir()
|
|
if not os.path.isdir(directory):
|
|
return []
|
|
return sorted(
|
|
name for name in os.listdir(directory)
|
|
if os.path.isdir(os.path.join(directory, name))
|
|
)
|
|
|
|
|
|
def _version_sort_key(version_string):
|
|
numbers = [int(part) for part in re.findall(r"\d+", version_string)]
|
|
return numbers or [0]
|
|
|
|
|
|
def kit_versions(kit_name):
|
|
"""Maps checksum -> version string, read from the .kit filenames under kits/."""
|
|
if kit_name in _versions_cache:
|
|
return _versions_cache[kit_name]
|
|
|
|
versions = {}
|
|
kit_dir = os.path.join(_kits_dir(), kit_name)
|
|
if os.path.isdir(kit_dir):
|
|
pattern = re.compile(rf"^{re.escape(kit_name)}-([0-9a-fA-F]+)-(.+)\.kit$")
|
|
for entry in os.listdir(kit_dir):
|
|
match = pattern.match(entry)
|
|
if match:
|
|
versions[match.group(1)] = match.group(2)
|
|
|
|
_versions_cache[kit_name] = versions
|
|
return versions
|
|
|
|
|
|
def resolve_manifest_path(kit_name):
|
|
"""Picks the manifest to describe a kit, returning (path, version_label).
|
|
|
|
Preference order:
|
|
1. the checksum the open file's <schema> pins, when it declares one
|
|
2. the highest real version number, via the .kit filenames
|
|
3. the last manifest by filename, which is what the app has always fallen back to
|
|
"""
|
|
kit_dir = os.path.join(_manifests_dir(), kit_name)
|
|
if not os.path.isdir(kit_dir):
|
|
return None, ""
|
|
|
|
manifests = sorted(
|
|
entry for entry in os.listdir(kit_dir)
|
|
if entry.startswith(f"{kit_name}-") and entry.endswith(".xml")
|
|
)
|
|
if not manifests:
|
|
return None, ""
|
|
|
|
versions = kit_versions(kit_name)
|
|
|
|
pinned = app_state.active_schema_kits.get(kit_name)
|
|
if pinned:
|
|
candidate = f"{kit_name}-{pinned}.xml"
|
|
if candidate in manifests:
|
|
return os.path.join(kit_dir, candidate), versions.get(pinned, pinned)
|
|
|
|
available = []
|
|
for entry in manifests:
|
|
checksum = entry[len(kit_name) + 1:-4]
|
|
if checksum in versions:
|
|
available.append((_version_sort_key(versions[checksum]), entry, versions[checksum]))
|
|
|
|
if available:
|
|
available.sort()
|
|
_, entry, version_label = available[-1]
|
|
return os.path.join(kit_dir, entry), version_label
|
|
|
|
entry = manifests[-1]
|
|
checksum = entry[len(kit_name) + 1:-4]
|
|
return os.path.join(kit_dir, entry), versions.get(checksum, "")
|
|
|
|
|
|
def _load_kit(kit_name):
|
|
"""Parses a kit's chosen manifest into (version label, {type name: element})."""
|
|
if kit_name in _manifest_cache:
|
|
return _manifest_cache[kit_name]
|
|
|
|
path, version_label = resolve_manifest_path(kit_name)
|
|
types = {}
|
|
if path:
|
|
try:
|
|
root = ET.parse(path).getroot()
|
|
for type_element in root.findall("type"):
|
|
types[type_element.get("name")] = type_element
|
|
except Exception as error:
|
|
print(f"Palette: cannot read manifest {path}: {error}")
|
|
|
|
_manifest_cache[kit_name] = (version_label, types)
|
|
return _manifest_cache[kit_name]
|
|
|
|
|
|
def kit_version_label(kit_name):
|
|
return _load_kit(kit_name)[0]
|
|
|
|
|
|
def resolve_checksum(kit_name):
|
|
"""Checksum of the manifest this palette resolved for a kit, or None."""
|
|
path, _ = resolve_manifest_path(kit_name)
|
|
if not path:
|
|
return None
|
|
filename = os.path.basename(path)
|
|
if not filename.startswith(f"{kit_name}-") or not filename.endswith(".xml"):
|
|
return None
|
|
return filename[len(kit_name) + 1:-4]
|
|
|
|
|
|
def _find_type(qualified_name):
|
|
if "::" not in qualified_name:
|
|
return None
|
|
kit_name, type_name = qualified_name.split("::", 1)
|
|
return _load_kit(kit_name)[1].get(type_name)
|
|
|
|
|
|
def _is_component(qualified_name, seen=None):
|
|
"""True when the base chain reaches sys::Component, so the type can live in an app."""
|
|
if qualified_name == "sys::Component":
|
|
return True
|
|
if qualified_name in _component_cache:
|
|
return _component_cache[qualified_name]
|
|
|
|
seen = seen or set()
|
|
if qualified_name in seen:
|
|
return False
|
|
seen.add(qualified_name)
|
|
|
|
type_element = _find_type(qualified_name)
|
|
if type_element is None:
|
|
result = False
|
|
else:
|
|
base = type_element.get("base")
|
|
result = bool(base) and _is_component(base, seen)
|
|
|
|
_component_cache[qualified_name] = result
|
|
return result
|
|
|
|
|
|
def list_component_types(kit_name):
|
|
"""Concrete component types in a kit, as (type name, base) pairs.
|
|
|
|
Primitives (no base) and abstract types (the 'a' flag) are left out — neither can be
|
|
placed in an application.
|
|
"""
|
|
_, types = _load_kit(kit_name)
|
|
|
|
entries = []
|
|
for type_name, type_element in types.items():
|
|
if "a" in (type_element.get("flags") or ""):
|
|
continue
|
|
if not _is_component(f"{kit_name}::{type_name}"):
|
|
continue
|
|
entries.append((type_name, type_element.get("base") or ""))
|
|
|
|
return sorted(entries)
|
|
|
|
|
|
def invalidate():
|
|
"""Clears every cache, for when Sedona home changes in Preferences."""
|
|
_versions_cache.clear()
|
|
_manifest_cache.clear()
|
|
_component_cache.clear()
|