11"""
2- attrs.json — user-defined attribute serialization.
2+ attrs.bin — user-defined attribute serialization.
33
4- Format: JSON object
4+ Format v1 (legacy) : JSON object
55 {"version": 1, "entries": [{"idx": <int>, "attrs": {<key>: <val>}}, ...]}
66
7- Only scopes that have at least one attribute are included (sparse).
7+ Format v2 (current): JSON object with sections for scopes, coveritems,
8+ history nodes, and global attrs.
9+ {"version": 2,
10+ "scopes": [{"idx": <int>, "attrs": {<key>: <val>}}, ...],
11+ "coveritems": [{"scope_idx": <int>, "ci_idx": <int>, "attrs": {...}}, ...],
12+ "history": [{"idx": <int>, "attrs": {...}}, ...],
13+ "global": {<key>: <val>}}
814"""
915
1016import json
1117
1218from .dfs_util import dfs_scope_list
19+ from ucis .history_node_kind import HistoryNodeKind
1320
14- _VERSION = 1
21+ _VERSION = 2
22+ _COVER_ALL = 0xFFFFFFFF
1523
1624
1725class AttrsWriter :
18- """Serialize user-defined scope attributes to attrs.json bytes."""
26+ """Serialize user-defined attributes to attrs.bin bytes."""
1927
2028 def serialize (self , db ) -> bytes :
2129 scopes = dfs_scope_list (db )
22- entries = []
30+ scope_entries = []
2331 for idx , scope in enumerate (scopes ):
2432 if not hasattr (scope , 'getAttributes' ):
2533 continue
2634 attrs = scope .getAttributes ()
2735 if attrs :
28- entries .append ({"idx" : idx , "attrs" : attrs })
29- payload = {"version" : _VERSION , "entries" : entries }
36+ scope_entries .append ({"idx" : idx , "attrs" : attrs })
37+
38+ ci_entries = []
39+ for idx , scope in enumerate (scopes ):
40+ try :
41+ items = list (scope .coverItems (_COVER_ALL ))
42+ except Exception :
43+ continue
44+ for ci_idx , ci in enumerate (items ):
45+ if not hasattr (ci , 'getAttributes' ):
46+ continue
47+ attrs = ci .getAttributes ()
48+ if attrs :
49+ ci_entries .append ({
50+ "scope_idx" : idx , "ci_idx" : ci_idx , "attrs" : attrs
51+ })
52+
53+ hist_entries = []
54+ for kind in (HistoryNodeKind .TEST , HistoryNodeKind .MERGE ):
55+ try :
56+ nodes = list (db .historyNodes (kind ))
57+ except Exception :
58+ continue
59+ for hi , node in enumerate (nodes ):
60+ if not hasattr (node , 'getAttributes' ):
61+ continue
62+ attrs = node .getAttributes ()
63+ if attrs :
64+ hist_entries .append ({
65+ "idx" : hi , "kind" : kind .name , "attrs" : attrs
66+ })
67+
68+ global_attrs = {}
69+ if hasattr (db , 'getAttributes' ):
70+ global_attrs = db .getAttributes ()
71+
72+ payload = {
73+ "version" : _VERSION ,
74+ "scopes" : scope_entries ,
75+ "coveritems" : ci_entries ,
76+ "history" : hist_entries ,
77+ "global" : global_attrs ,
78+ }
3079 return json .dumps (payload , separators = (',' , ':' )).encode ()
3180
3281
3382class AttrsReader :
34- """Deserialize attrs.json bytes and apply attributes to scope tree ."""
83+ """Deserialize attrs.bin bytes and apply attributes."""
3584
3685 def deserialize (self , data : bytes , db ) -> None :
3786 if not data :
3887 return
3988 payload = json .loads (data .decode ())
40- if payload .get ("version" ) != _VERSION :
41- raise ValueError (f"Unsupported attrs.json version: { payload .get ('version' )} " )
89+ version = payload .get ("version" , 1 )
90+
91+ if version == 1 :
92+ self ._deserialize_v1 (payload , db )
93+ elif version == 2 :
94+ self ._deserialize_v2 (payload , db )
95+
96+ def _deserialize_v1 (self , payload , db ):
97+ """Legacy v1: scope attrs only."""
4298 entries = payload .get ("entries" , [])
4399 if not entries :
44100 return
@@ -50,3 +106,54 @@ def deserialize(self, data: bytes, db) -> None:
50106 for key , val in entry .get ("attrs" , {}).items ():
51107 if hasattr (scope , 'setAttribute' ):
52108 scope .setAttribute (key , val )
109+
110+ def _deserialize_v2 (self , payload , db ):
111+ """V2: scopes + coveritems + history + global."""
112+ scopes = dfs_scope_list (db )
113+
114+ for entry in payload .get ("scopes" , []):
115+ idx = entry ["idx" ]
116+ if idx < len (scopes ):
117+ scope = scopes [idx ]
118+ for key , val in entry .get ("attrs" , {}).items ():
119+ if hasattr (scope , 'setAttribute' ):
120+ scope .setAttribute (key , val )
121+
122+ for entry in payload .get ("coveritems" , []):
123+ scope_idx = entry ["scope_idx" ]
124+ ci_idx = entry ["ci_idx" ]
125+ if scope_idx < len (scopes ):
126+ scope = scopes [scope_idx ]
127+ try :
128+ items = list (scope .coverItems (_COVER_ALL ))
129+ if ci_idx < len (items ):
130+ ci = items [ci_idx ]
131+ for key , val in entry .get ("attrs" , {}).items ():
132+ if hasattr (ci , 'setAttribute' ):
133+ ci .setAttribute (key , val )
134+ except Exception :
135+ pass
136+
137+ hist_nodes = {}
138+ for kind in (HistoryNodeKind .TEST , HistoryNodeKind .MERGE ):
139+ try :
140+ hist_nodes [kind .name ] = list (db .historyNodes (kind ))
141+ except Exception :
142+ pass
143+ for entry in payload .get ("history" , []):
144+ kind_name = entry .get ("kind" , "TEST" )
145+ idx = entry ["idx" ]
146+ nodes = hist_nodes .get (kind_name , [])
147+ if idx < len (nodes ):
148+ node = nodes [idx ]
149+ for key , val in entry .get ("attrs" , {}).items ():
150+ if hasattr (node , 'setAttribute' ):
151+ node .setAttribute (key , val )
152+
153+ for key , val in payload .get ("global" , {}).items ():
154+ if hasattr (db , 'setAttribute' ):
155+ db .setAttribute (key , val )
156+
157+ def apply (self , db , data : bytes ) -> None :
158+ """Alias for deserialize (matches other readers' API)."""
159+ self .deserialize (data , db )
0 commit comments