Skip to content

Commit 29e9e8b

Browse files
committed
Add helper to regenerate AttributeDefinitions in GDTF, update docs
1 parent 7dbdf12 commit 29e9e8b

File tree

8 files changed

+4161
-23
lines changed

8 files changed

+4161
-23
lines changed

AttributeDefinitions.xml

Lines changed: 481 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,23 @@ uv run pytest
160160
uv run pytest --mypy -m mypy pygdtf/*py
161161
```
162162

163+
## Updating Attribute Definitions
164+
165+
The canonical AttributeDefinitions XML lives at the repo root:
166+
`AttributeDefinitions.xml`. When the GDTF spec updates that file, regenerate
167+
the baked Python data module used for zero-IO startup:
168+
169+
```bash
170+
python3 - <<'PY'
171+
from pygdtf.utils import attr_loader
172+
attr_loader.generate_attribute_definitions_module()
173+
PY
174+
```
175+
176+
This writes `pygdtf/utils/attribute_definitions_data.py`, which is imported at
177+
runtime instead of reading the XML. Commit the regenerated module along with
178+
the updated XML.
179+
163180
## Citation
164181

165182
If you use this library in your research, publication, or software project,

pygdtf/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2323
# SOFTWARE.
2424

25+
import copy
2526
import datetime
2627
import zipfile
2728
import sys
@@ -40,7 +41,7 @@
4041
from .utils import *
4142
from .value import * # type: ignore
4243

43-
__version__ = "1.4.0-dev0"
44+
__version__ = "1.4.0-dev1"
4445

4546
# Standard predefined colour spaces: R, G, B, W-P
4647
COLOR_SPACE_SRGB = ColorSpaceDefinition(
@@ -927,6 +928,8 @@ def __init__(
927928
self.luminous_intensity = luminous_intensity
928929
self.transmission = transmission
929930
self.interpolation_to = interpolation_to
931+
# Tracks which optional/defaulted attributes were explicitly set so we
932+
# can re-emit them during serialization without bloating everything else.
930933
self._attr_keys: set = set()
931934
super().__init__(*args, **kwargs)
932935

@@ -1881,6 +1884,8 @@ def __init__(
18811884
self.sub_channel_sets = sub_channel_sets
18821885
else:
18831886
self.sub_channel_sets = []
1887+
# Tracks which optional/defaulted attributes were explicitly set so we
1888+
# can re-emit them during serialization without bloating everything else.
18841889
self._attr_keys: set = set()
18851890
super().__init__(*args, **kwargs)
18861891

pygdtf/utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
from typing import Any, Dict, List, Optional
2828

2929
import pygdtf
30+
from .attr_loader import generate_attribute_definitions_module
31+
from .attribute_regenerator import regenerate_attribute_definitions
3032

3133

3234
def _get_channels_by_geometry(

pygdtf/utils/attr_loader.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import json
2+
import re
3+
from functools import lru_cache
4+
from pathlib import Path
5+
from typing import Any, Dict, List, Optional
6+
from xml.etree import ElementTree
7+
8+
9+
def _annex_attr_path() -> Path:
10+
"""Return path to structured Annex attribute XML."""
11+
return Path(__file__).resolve().parents[2] / "AttributeDefinitions.xml"
12+
13+
14+
def _parse_attribute_definitions_xml(xml_path: Path) -> Dict[str, Any]:
15+
root = ElementTree.parse(xml_path).getroot()
16+
17+
activation_group_nodes = root.find("ActivationGroups")
18+
activation_groups = [
19+
ag.attrib.get("Name")
20+
for ag in (
21+
activation_group_nodes.findall("ActivationGroup")
22+
if activation_group_nodes is not None
23+
else []
24+
)
25+
if ag.attrib.get("Name")
26+
]
27+
28+
feature_group_nodes = root.find("FeatureGroups")
29+
feature_groups = []
30+
if feature_group_nodes is not None:
31+
for feature_group in feature_group_nodes.findall("FeatureGroup"):
32+
feature_groups.append(
33+
{
34+
"name": feature_group.attrib.get("Name"),
35+
"pretty": feature_group.attrib.get("Pretty"),
36+
"features": [
37+
feature.attrib.get("Name")
38+
for feature in feature_group.findall("Feature")
39+
if feature.attrib.get("Name")
40+
],
41+
}
42+
)
43+
44+
attribute_nodes = root.find("Attributes")
45+
attributes = []
46+
if attribute_nodes is not None:
47+
for attr in attribute_nodes.findall("Attribute"):
48+
attributes.append(
49+
{
50+
"name": attr.attrib.get("Name"),
51+
"pretty": attr.attrib.get("Pretty"),
52+
"activation_group": attr.attrib.get("ActivationGroup"),
53+
"feature": attr.attrib.get("Feature"),
54+
"main_attribute": attr.attrib.get("MainAttribute"),
55+
"physical_unit": attr.attrib.get("PhysicalUnit"),
56+
"color": attr.attrib.get("Color"),
57+
"subphysical_units": [
58+
{
59+
"Type": spu.attrib.get("Type"),
60+
"PhysicalUnit": spu.attrib.get("PhysicalUnit"),
61+
"PhysicalFrom": spu.attrib.get("PhysicalFrom"),
62+
"PhysicalTo": spu.attrib.get("PhysicalTo"),
63+
}
64+
for spu in attr.findall("SubPhysicalUnit")
65+
],
66+
}
67+
)
68+
69+
return {
70+
"activation_groups": activation_groups,
71+
"feature_groups": feature_groups,
72+
"attributes": attributes,
73+
}
74+
75+
76+
def _build_templates_from_data(data: Dict[str, Any]) -> Dict[str, Any]:
77+
activation_groups = data.get("activation_groups", [])
78+
feature_groups = data.get("feature_groups", [])
79+
80+
attributes_exact: Dict[str, Dict[str, Any]] = {}
81+
attributes_wildcard: List[Dict[str, Any]] = []
82+
83+
for attr in data.get("attributes", []):
84+
template = {
85+
"name": attr.get("name"),
86+
"pretty": attr.get("pretty"),
87+
"activation_group": attr.get("activation_group"),
88+
"feature": attr.get("feature"),
89+
"main_attribute": attr.get("main_attribute"),
90+
"physical_unit": attr.get("physical_unit"),
91+
"color": attr.get("color"),
92+
"subphysical_units": attr.get("subphysical_units", []),
93+
}
94+
name = template["name"]
95+
if not name:
96+
continue
97+
if "(n)" in name or "(m)" in name:
98+
pattern = re.escape(name)
99+
pattern = pattern.replace("\\(n\\)", "(?P<n>\\d+)")
100+
pattern = pattern.replace("\\(m\\)", "(?P<m>\\d+)")
101+
attributes_wildcard.append(
102+
{
103+
**template,
104+
"pattern": re.compile(f"^{pattern}$"),
105+
}
106+
)
107+
else:
108+
attributes_exact[name] = template
109+
110+
return {
111+
"activation_groups": activation_groups,
112+
"feature_groups": feature_groups,
113+
"attributes": {
114+
"exact": attributes_exact,
115+
"wildcard": attributes_wildcard,
116+
},
117+
}
118+
119+
120+
@lru_cache(maxsize=1)
121+
def _load_annex_attribute_templates():
122+
"""Load Annex attribute definitions from generated module or XML."""
123+
try:
124+
from . import attribute_definitions_data as _attr_data_module
125+
126+
raw_data = getattr(_attr_data_module, "ANNEX_ATTRIBUTE_DEFINITIONS", None)
127+
except Exception:
128+
raw_data = None
129+
130+
if raw_data is None:
131+
raw_data = _parse_attribute_definitions_xml(_annex_attr_path())
132+
133+
return _build_templates_from_data(raw_data)
134+
135+
136+
def generate_attribute_definitions_module(
137+
xml_path: Optional[Path] = None, output_path: Optional[Path] = None
138+
) -> Path:
139+
"""
140+
Generate a Python module with AttributeDefinitions data for zero-IO startup.
141+
142+
The generated module exports ANNEX_ATTRIBUTE_DEFINITIONS matching the XML content.
143+
"""
144+
source = Path(xml_path) if xml_path is not None else _annex_attr_path()
145+
target = (
146+
Path(output_path)
147+
if output_path is not None
148+
else Path(__file__).resolve().parent / "attribute_definitions_data.py"
149+
)
150+
151+
data = _parse_attribute_definitions_xml(source)
152+
# Use repr so generated file is valid Python (no JSON null/true/false).
153+
content = (
154+
"# Auto-generated from AttributeDefinitions XML; do not edit by hand.\n"
155+
f"# Source: {source.name}\n"
156+
"ANNEX_ATTRIBUTE_DEFINITIONS = " + repr(data) + "\n"
157+
)
158+
target.write_text(content, encoding="utf-8")
159+
return target

0 commit comments

Comments
 (0)