2
2
#
3
3
# SPDX-License-Identifier: MIT
4
4
5
+ import ast
5
6
import os
7
+ import re
6
8
import sys
7
- import astroid
8
9
import traceback
9
10
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
+
12
78
13
79
def convert_folder (top_level , stub_directory ):
14
80
ok = 0
15
81
total = 0
16
82
filenames = sorted (os .listdir (top_level ))
17
83
pyi_lines = []
84
+
18
85
for filename in filenames :
19
86
full_path = os .path .join (top_level , filename )
20
87
file_lines = []
21
88
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 ))
23
90
ok += mok
24
91
total += mtotal
25
92
elif filename .endswith (".c" ):
@@ -44,44 +111,57 @@ def convert_folder(top_level, stub_directory):
44
111
pyi_lines .extend (file_lines )
45
112
46
113
if not pyi_lines :
47
- return ok , total
114
+ return ( ok , total )
48
115
49
116
stub_filename = os .path .join (stub_directory , "__init__.pyi" )
50
117
print (stub_filename )
51
118
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 )
55
119
56
120
# Validate that the module is a parseable stub.
57
121
total += 1
58
122
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 )
75
126
ok += 1
76
- except astroid .exceptions .AstroidSyntaxError as e :
77
- e = e .__cause__
127
+ except SyntaxError as e :
78
128
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
+
79
154
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 ]
81
161
82
- ok , total = convert_folder (top_level , stub_directory )
162
+ ( ok , total ) = convert_folder (top_level , stub_directory )
83
163
84
- print (f"{ ok } ok out of { total } " )
164
+ print (f"Parsing .pyi files: { total - ok } failed, { ok } passed " )
85
165
86
- if ok != total :
87
- sys .exit (total - ok )
166
+ if ok != total :
167
+ sys .exit (total - ok )
0 commit comments