22#
33# SPDX-License-Identifier: MIT
44
5+ import ast
56import os
7+ import re
68import sys
7- import astroid
89import traceback
910
10- top_level = sys .argv [1 ].strip ("/" )
11- stub_directory = sys .argv [2 ]
11+ import isort
12+
13+
14+ IMPORTS_IGNORE = frozenset ({'int' , 'float' , 'bool' , 'str' , 'bytes' , 'tuple' , 'list' , 'set' , 'dict' , 'bytearray' , 'file' , 'buffer' })
15+ IMPORTS_TYPING = frozenset ({'Any' , 'Optional' , 'Union' , 'Tuple' , 'List' , 'Sequence' })
16+ IMPORTS_TYPESHED = frozenset ({'ReadableBuffer' , 'WritableBuffer' })
17+
18+
19+ def is_any (node ):
20+ node_type = type (node )
21+ if node is None :
22+ return True
23+ if node_type == ast .Name and node .id == "Any" :
24+ return True
25+ if (node_type == ast .Attribute and type (node .value ) == ast .Name
26+ and node .value .id == "typing" and node .attr == "Any" ):
27+ return True
28+ return False
29+
30+
31+ def report_missing_annotations (tree ):
32+ for node in ast .walk (tree ):
33+ node_type = type (node )
34+ if node_type == ast .AnnAssign :
35+ if is_any (node .annotation ):
36+ print (f"Missing attribute type on line { node .lineno } " )
37+ elif node_type == ast .arg :
38+ if is_any (node .annotation ) and node .arg != "self" :
39+ print (f"Missing argument type: { node .arg } on line { node .lineno } " )
40+ elif node_type == ast .FunctionDef :
41+ if is_any (node .returns ) and node .name != "__init__" :
42+ print (f"Missing return type: { node .name } on line { node .lineno } " )
43+
44+
45+ def extract_imports (tree ):
46+ modules = set ()
47+ typing = set ()
48+ typeshed = set ()
49+
50+ def collect_annotations (anno_tree ):
51+ if anno_tree is None :
52+ return
53+ for node in ast .walk (anno_tree ):
54+ node_type = type (node )
55+ if node_type == ast .Name :
56+ if node .id in IMPORTS_IGNORE :
57+ continue
58+ elif node .id in IMPORTS_TYPING :
59+ typing .add (node .id )
60+ elif node .id in IMPORTS_TYPESHED :
61+ typeshed .add (node .id )
62+ elif not node .id [0 ].isupper ():
63+ modules .add (node .id )
64+
65+ for node in ast .walk (tree ):
66+ node_type = type (node )
67+ if (node_type == ast .AnnAssign ) or (node_type == ast .arg ):
68+ collect_annotations (node .annotation )
69+ elif node_type == ast .FunctionDef :
70+ collect_annotations (node .returns )
71+
72+ return {
73+ "modules" : sorted (modules ),
74+ "typing" : sorted (typing ),
75+ "typeshed" : sorted (typeshed ),
76+ }
77+
1278
1379def convert_folder (top_level , stub_directory ):
1480 ok = 0
1581 total = 0
1682 filenames = sorted (os .listdir (top_level ))
1783 pyi_lines = []
84+
1885 for filename in filenames :
1986 full_path = os .path .join (top_level , filename )
2087 file_lines = []
2188 if os .path .isdir (full_path ):
22- mok , mtotal = convert_folder (full_path , os .path .join (stub_directory , filename ))
89+ ( mok , mtotal ) = convert_folder (full_path , os .path .join (stub_directory , filename ))
2390 ok += mok
2491 total += mtotal
2592 elif filename .endswith (".c" ):
@@ -44,44 +111,57 @@ def convert_folder(top_level, stub_directory):
44111 pyi_lines .extend (file_lines )
45112
46113 if not pyi_lines :
47- return ok , total
114+ return ( ok , total )
48115
49116 stub_filename = os .path .join (stub_directory , "__init__.pyi" )
50117 print (stub_filename )
51118 stub_contents = "" .join (pyi_lines )
52- os .makedirs (stub_directory , exist_ok = True )
53- with open (stub_filename , "w" ) as f :
54- f .write (stub_contents )
55119
56120 # Validate that the module is a parseable stub.
57121 total += 1
58122 try :
59- tree = astroid .parse (stub_contents )
60- for i in tree .body :
61- if 'name' in i .__dict__ :
62- print (i .__dict__ ['name' ])
63- for j in i .body :
64- if isinstance (j , astroid .scoped_nodes .FunctionDef ):
65- if None in j .args .__dict__ ['annotations' ]:
66- print (f"Missing parameter type: { j .__dict__ ['name' ]} on line { j .__dict__ ['lineno' ]} \n " )
67- if j .returns :
68- if 'Any' in j .returns .__dict__ .values ():
69- print (f"Missing return type: { j .__dict__ ['name' ]} on line { j .__dict__ ['lineno' ]} " )
70- elif isinstance (j , astroid .node_classes .AnnAssign ):
71- if 'name' in j .__dict__ ['annotation' ].__dict__ :
72- if j .__dict__ ['annotation' ].__dict__ ['name' ] == 'Any' :
73- print (f"missing attribute type on line { j .__dict__ ['lineno' ]} " )
74-
123+ tree = ast .parse (stub_contents )
124+ imports = extract_imports (tree )
125+ report_missing_annotations (tree )
75126 ok += 1
76- except astroid .exceptions .AstroidSyntaxError as e :
77- e = e .__cause__
127+ except SyntaxError as e :
78128 traceback .print_exception (type (e ), e , e .__traceback__ )
129+ return (ok , total )
130+
131+ # Add import statements
132+ import_lines = ["from __future__ import annotations" ]
133+ import_lines .extend (f"import { m } " for m in imports ["modules" ])
134+ import_lines .append ("from typing import " + ", " .join (imports ["typing" ]))
135+ import_lines .append ("from _typeshed import " + ", " .join (imports ["typeshed" ]))
136+ import_body = "\n " .join (import_lines )
137+ m = re .match (r'(\s*""".*?""")' , stub_contents , flags = re .DOTALL )
138+ if m :
139+ stub_contents = m .group (1 ) + "\n \n " + import_body + "\n \n " + stub_contents [m .end ():]
140+ else :
141+ stub_contents = import_body + "\n \n " + stub_contents
142+ stub_contents = isort .code (stub_contents )
143+
144+ # Adjust blank lines
145+ stub_contents = re .sub (r"\n+class" , "\n \n \n class" , stub_contents )
146+ stub_contents = re .sub (r"\n+def" , "\n \n \n def" , stub_contents )
147+ stub_contents = re .sub (r"\n+^(\s+)def" , lambda m : f"\n \n { m .group (1 )} def" , stub_contents , flags = re .M )
148+ stub_contents = stub_contents .strip () + "\n "
149+
150+ os .makedirs (stub_directory , exist_ok = True )
151+ with open (stub_filename , "w" ) as f :
152+ f .write (stub_contents )
153+
79154 print ()
80- return ok , total
155+ return (ok , total )
156+
157+
158+ if __name__ == "__main__" :
159+ top_level = sys .argv [1 ].strip ("/" )
160+ stub_directory = sys .argv [2 ]
81161
82- ok , total = convert_folder (top_level , stub_directory )
162+ ( ok , total ) = convert_folder (top_level , stub_directory )
83163
84- print (f"{ ok } ok out of { total } " )
164+ print (f"Parsing .pyi files: { total - ok } failed, { ok } passed " )
85165
86- if ok != total :
87- sys .exit (total - ok )
166+ if ok != total :
167+ sys .exit (total - ok )
0 commit comments