88from awl import jsonld_context
99
1010
11+ class ASTNotAModule (Exception ):
12+ """Raised when the root node of the parsed object is **not** an ast.Module."""
13+
14+
1115class AstSerialization :
12- def __init__ (self ):
13- pass
16+ def __init__ (self , annotate : bool = False , backparsable : bool = False ) -> None :
17+ """
18+ Initializes AstSerialization with parser options.
19+
20+ Parameters
21+ ----------
22+ annotate : bool, default False
23+ If ``True``, annotates the AST tree
24+ backparsable : bool, default False
25+ If ``True``, AST tree is unparsable via self.unparse
26+ If ``False``, Annotations deletes that are required for unparsing,
27+ resulting in a neat tree
28+ """
29+ self .annotate = annotate
30+ self .backparsable = backparsable
1431
1532 @staticmethod
1633 def del_keys (d : dict , keys : list ) -> dict :
@@ -55,6 +72,10 @@ def parse(self, source: str) -> dict:
5572
5673 ast_dict = self .del_keys (ast_dict , rm_keywords ) # remove annotations
5774 self .ast_dict = ast_dict
75+
76+ if self .annotate :
77+ self .annotate_ast ()
78+
5879 return ast_dict
5980
6081 def unparse (self , ast_dict : dict = None ) -> str :
@@ -71,6 +92,156 @@ def dumps(self, format="yaml") -> str:
7192 res = yaml .dump (self .ast_dict , indent = 4 )
7293 return res
7394
95+ def annotate_ast (self ) -> None :
96+ """Validate the root node and start annotation walk.
97+ Raises
98+ ------
99+ ASTNotAModule
100+ If the parsed tree does **not** start with an ``ast.Module`` node.
101+ """
102+ # todo add further veryfication
103+ if self .ast_dict .get ("_type" ) != "Module" :
104+ raise ASTNotAModule ("root node is not a Module" )
105+ self ._walk_json_ast (self .ast_dict , path = None )
106+
107+ def _walk_json_ast (self , node : list | dict | object , path : list ) -> None :
108+ """Depth‑first traversal of *node* while keeping track of *path*.
109+
110+ Parameters
111+ ----------
112+ node
113+ Current AST sub‑node (``dict``, ``list`` or scalar).
114+ path
115+ Accumulated list of keys / indices leading from the root to *node*.
116+ """
117+
118+ if path is None :
119+ path = []
120+ # ------------------------------------------------------------------ #
121+ # 1.Recursive walk
122+ # ------------------------------------------------------------------ #
123+ elif isinstance (node , list ):
124+ # print(f"Path: {path}")
125+ for index , item in enumerate (node ):
126+ self ._walk_json_ast (item , path + [index ])
127+
128+ if isinstance (node , dict ):
129+ # print(f"Path: {path}")
130+ for key , value in node .items ():
131+ self ._walk_json_ast (value , path + [key ])
132+
133+ # Primitive leaf – nothing to do
134+ else :
135+ # print(f"Path: {path} -> Value: {node}")
136+ pass
137+
138+ # ------------------------------------------------------------------ #
139+ # 2.Collapse handles the replacement logic to from leaf to "stem"
140+ # ------------------------------------------------------------------ #
141+
142+ # This checks for the class constructor syntax in AST
143+ # e.g "value":
144+ # {"_type": "Call","args": [],"func": {"_type": "Name","id": "ClassA"}
145+ if isinstance (node , dict ):
146+ if (
147+ node .get ("_type" ) == "Call" # A Constructor is a call
148+ and node .get ("func" , {}).get ("_type" )
149+ == "Name" # A Constructor is a call of type Name
150+ and (
151+ fid := node .get ("func" , {}).get ("id" )
152+ ) # fid is None if the path is missing and hence False
153+ and fid [0 ].isupper () # only runs if fid is truthy,
154+ # wont give TypeError/IndexError
155+ ):
156+ ctor_node = AstSerialization ._get_from_path (self .ast_dict , path )
157+
158+ ctor_node ["__class_name__" ] = fid
159+ # self.ast_dict["__class_name__"] = fid
160+ # print (fid)
161+
162+ for kw_node in node ["keywords" ]:
163+ if isinstance (kw_node , dict ):
164+ if kw_node .get ("_type" ) == "keyword" :
165+ ctor_node [kw_node ["arg" ]] = self ._val (kw_node ["value" ])
166+
167+ if self .backparsable is False :
168+ # slim notation
169+ ctor_node = AstSerialization .slim_notation (ctor_node )
170+
171+ @staticmethod
172+ def _val (node : list | dict | object ) -> object | None :
173+ """Convert AST *value* nodes into primitives or nested constructor annotations.
174+
175+ Returns
176+ -------
177+ object | None
178+ * ``int``, ``str`` … for ``Constant`` nodes;
179+ * dotted ``str`` for ``Attribute`` chains;
180+ * nested constructor annotations (dict) for embedded calls;
181+ * ``None`` for values that are irrelevant / not serialisable.
182+ """
183+
184+ if isinstance (node , dict ):
185+ # todo currently f(a=t) and f(a="t") have same annotation,
186+ # think about if this can lead to problems
187+ t = node .get ("_type" )
188+ ctor = node .get ("__class_name__" )
189+ # f(a=1) :"value": {"_type": "Constant","value": 1}
190+ if t == "Constant" :
191+ return node ["value" ]
192+ # f(a=t) : "value": {"_type": "Name","id": "t"}
193+ if t == "Name" :
194+ return node ["id" ]
195+ # f(a = U.V) : "value":
196+ # {"_type": "Attribute","attr": "V","value": {"_type": "Name","id": "U"}}
197+ if t == "Attribute" :
198+ return AstSerialization ._attr_to_str (node )
199+ if ctor :
200+ return AstSerialization .slim_notation (node .copy ())
201+ return None
202+
203+ # ------------------------------------------------------------------ #
204+ # Attribute -> dotted string
205+ # ------------------------------------------------------------------ #
206+ @staticmethod
207+ def _attr_to_str (node : dict ) -> str :
208+ """Flatten a chain of ``Attribute``/``Name`` nodes into ``"U.V"``."""
209+ # f(a = U.V) : "value":
210+ # {"_type": "Attribute","attr": "V","value": {"_type": "Name","id": "U"}}
211+ parts : list [str ] = []
212+
213+ def walk (n ):
214+ if n ["_type" ] == "Attribute" :
215+ walk (n ["value" ])
216+ parts .append (n ["attr" ])
217+ elif n ["_type" ] == "Name" :
218+ parts .append (n ["id" ])
219+
220+ walk (node )
221+ return "." .join (parts )
222+
223+ @staticmethod
224+ def _get_from_path (node : list | dict | object , path : list ) -> list | dict | object :
225+ """Return the sub‑node referenced by *path*."""
226+ for key in path :
227+ node = node [key ]
228+ return node
229+
230+ @staticmethod
231+ def _dump_from_path (node : list | dict | object , path : list ) -> str :
232+ """Pretty JSON dump of the sub‑node at *path* (debug helper)."""
233+ node = AstSerialization ._get_from_path (node , path )
234+ res = json .dumps (node , indent = 4 )
235+ return res
236+
237+ @staticmethod
238+ def slim_notation (node : list | dict | object ) -> list | dict | object :
239+ """pops the unnecessary parameters of a constructor
240+ and returns slim notation node"""
241+ for k in ("_type" , "args" , "func" , "keywords" ):
242+ node .pop (k , None )
243+ return node
244+
74245 def to_jsonld (self ) -> dict :
75246 res = {"@context" : jsonld_context .awl_context ["@context" ], ** self .ast_dict }
76247 return res
0 commit comments