55import sys
66from collections .abc import Sequence
77from dataclasses import dataclass
8- from keyword import kwlist
98from pathlib import Path
109from textwrap import indent
1110from tokenize import COMMENT as COMMENT_TOKEN
2221
2322@click .command ()
2423@click .argument ("paths" , nargs = - 1 , type = click .Path (exists = True ))
25- def update_html_usages (paths : list [str ]) -> None :
24+ def rewrite_key_declarations (paths : list [str ]) -> None :
2625 """Rewrite files under the given paths using the new html element API.
2726
2827 The old API required users to pass a dictionary of attributes to html element
@@ -64,9 +63,21 @@ def update_html_usages(paths: list[str]) -> None:
6463def generate_rewrite (file : Path , source : str ) -> str | None :
6564 tree = ast .parse (source )
6665
66+ changed = find_nodes_to_change (tree )
67+ if not changed :
68+ log_could_not_rewrite (file , tree )
69+ return None
70+
71+ new = rewrite_changed_nodes (file , source , tree , changed )
72+ log_could_not_rewrite (file , ast .parse (new ))
73+
74+ return new
75+
76+
77+ def find_nodes_to_change (tree : ast .AST ) -> list [Sequence [ast .AST ]]:
6778 changed : list [Sequence [ast .AST ]] = []
6879 for parents , node in walk_with_parent (tree ):
69- if not isinstance (node , ast .Call ):
80+ if not ( isinstance (node , ast .Call ) and node . keywords ):
7081 continue
7182
7283 func = node .func
@@ -77,34 +88,62 @@ def generate_rewrite(file: Path, source: str) -> str | None:
7788 else :
7889 continue
7990
91+ for kw in list (node .keywords ):
92+ if kw .arg == "key" :
93+ break
94+ else :
95+ continue
96+
97+ maybe_attr_dict_node = None
8098 if name == "vdom" :
81- if len (node .args ) < 2 :
82- continue
83- maybe_attr_dict_node = node .args [1 ]
84- # remove attr dict from new args
85- new_args = node .args [:1 ] + node .args [2 :]
99+ if len (node .args ) == 1 :
100+ # vdom("tag") need to add attr dict
101+ maybe_attr_dict_node = ast .Dict (keys = [], values = [])
102+ node .args .append (maybe_attr_dict_node )
103+ elif isinstance (node .args [1 ], (ast .Constant , ast .JoinedStr )):
104+ maybe_attr_dict_node = ast .Dict (keys = [], values = [])
105+ node .args .insert (1 , maybe_attr_dict_node )
106+ elif len (node .args ) >= 2 :
107+ maybe_attr_dict_node = node .args [1 ]
86108 elif hasattr (html , name ):
87109 if len (node .args ) == 0 :
88- continue
89- maybe_attr_dict_node = node .args [0 ]
90- # remove attr dict from new args
91- new_args = node .args [1 :]
110+ # vdom("tag") need to add attr dict
111+ maybe_attr_dict_node = ast .Dict (keys = [], values = [])
112+ node .args .append (maybe_attr_dict_node )
113+ elif isinstance (node .args [0 ], (ast .Constant , ast .JoinedStr )):
114+ maybe_attr_dict_node = ast .Dict (keys = [], values = [])
115+ node .args .insert (0 , maybe_attr_dict_node )
116+ else :
117+ maybe_attr_dict_node = node .args [0 ]
118+
119+ if not maybe_attr_dict_node :
120+ continue
121+
122+ if isinstance (maybe_attr_dict_node , ast .Dict ):
123+ maybe_attr_dict_node .keys .append (ast .Constant ("key" ))
124+ maybe_attr_dict_node .values .append (kw .value )
125+ elif (
126+ isinstance (maybe_attr_dict_node , ast .Call )
127+ and isinstance (maybe_attr_dict_node .func , ast .Name )
128+ and maybe_attr_dict_node .func .id == "dict"
129+ and isinstance (maybe_attr_dict_node .func .ctx , ast .Load )
130+ ):
131+ maybe_attr_dict_node .keywords .append (ast .keyword (arg = "key" , value = kw .value ))
92132 else :
93133 continue
94134
95- new_keyword_info = extract_keywords (maybe_attr_dict_node )
96- if new_keyword_info is not None :
97- if new_keyword_info .replace :
98- node .keywords = new_keyword_info .keywords
99- else :
100- node .keywords .extend (new_keyword_info .keywords )
135+ node .keywords .remove (kw )
136+ changed .append ((node , * parents ))
101137
102- node .args = new_args
103- changed .append ((node , * parents ))
138+ return changed
104139
105- if not changed :
106- return None
107140
141+ def rewrite_changed_nodes (
142+ file : str ,
143+ source : str ,
144+ tree : ast .AST ,
145+ changed : list [Sequence [ast .AST ]],
146+ ) -> str :
108147 ast .fix_missing_locations (tree )
109148
110149 lines = source .split ("\n " )
@@ -171,38 +210,25 @@ def generate_rewrite(file: Path, source: str) -> str | None:
171210 return "\n " .join (lines )
172211
173212
174- def extract_keywords (node : ast .AST ) -> KeywordInfo | None :
175- if isinstance (node , ast .Dict ):
176- keywords : list [ast .keyword ] = []
177- for k , v in zip (node .keys , node .values ):
178- if isinstance (k , ast .Constant ) and isinstance (k .value , str ):
179- if k .value == "tagName" :
180- # this is a vdom dict declaration
181- return None
182- keywords .append (ast .keyword (arg = conv_attr_name (k .value ), value = v ))
183- else :
184- return KeywordInfo (
185- replace = True ,
186- keywords = [ast .keyword (arg = None , value = node )],
187- )
188- return KeywordInfo (replace = False , keywords = keywords )
189- elif (
190- isinstance (node , ast .Call )
191- and isinstance (node .func , ast .Name )
192- and node .func .id == "dict"
193- and isinstance (node .func .ctx , ast .Load )
194- ):
195- keywords = [ast .keyword (arg = None , value = a ) for a in node .args ]
196- for kw in node .keywords :
197- if kw .arg == "tagName" :
198- # this is a vdom dict declaration
199- return None
200- if kw .arg is not None :
201- keywords .append (ast .keyword (arg = conv_attr_name (kw .arg ), value = kw .value ))
202- else :
203- keywords .append (kw )
204- return KeywordInfo (replace = False , keywords = keywords )
205- return None
213+ def log_could_not_rewrite (file : str , tree : ast .AST ) -> None :
214+ for node in ast .walk (tree ):
215+ if not (isinstance (node , ast .Call ) and node .keywords ):
216+ continue
217+
218+ func = node .func
219+ if isinstance (func , ast .Attribute ):
220+ name = func .attr
221+ elif isinstance (func , ast .Name ):
222+ name = func .id
223+ else :
224+ continue
225+
226+ if (
227+ name == "vdom"
228+ or hasattr (html , name )
229+ and any (kw .arg == "key" for kw in node .keywords )
230+ ):
231+ click .echo (f"Unable to rewrite usage at { file } :{ node .lineno } " )
206232
207233
208234def find_comments (lines : list [str ]) -> list [str ]:
@@ -223,11 +249,6 @@ def walk_with_parent(
223249 yield from walk_with_parent (child , parents )
224250
225251
226- def conv_attr_name (name : str ) -> str :
227- new_name = CAMEL_CASE_SUB_PATTERN .sub ("_" , name ).replace ("-" , "_" ).lower ()
228- return f"{ new_name } _" if new_name in kwlist else new_name
229-
230-
231252@dataclass
232253class KeywordInfo :
233254 replace : bool
0 commit comments