Skip to content

Commit 0a95537

Browse files
author
Roberto De Ioris
authored
Merge pull request 20tab#405 from prokopst/pyi-stubs
Script to generate pyi stubs.
2 parents fdb6a5f + 0d2a9a0 commit 0a95537

File tree

1 file changed

+152
-0
lines changed

1 file changed

+152
-0
lines changed

tools/generate_pyi_stubs.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import re
2+
import os
3+
import sys
4+
import unreal_engine as ue
5+
6+
7+
# TODO: there are invalid names like 'IsInAir?' and 'EUserInterfaceActionType.None'
8+
VALID_NAME_PATTERN = re.compile('^[_a-zA-Z][_a-zA-Z0-9]*$')
9+
FILTERED_NAMES = {
10+
'__new__', '__doc__',
11+
'__str__', '__repr__', '__name__',
12+
'__loader__', '__spec__', '__package__',
13+
'__loader__', '__hash__', '__weakref__'
14+
}
15+
16+
# This list is kinda reliable for descriptors implemented in C, it's ugly to get their types
17+
# https://docs.python.org/3/library/inspect.html#fetching-attributes-statically
18+
DESCRIPTORS = {'getset_descriptor', 'member_descriptor'}
19+
20+
21+
def is_valid_name(name):
22+
return VALID_NAME_PATTERN.match(name) is not None and name not in FILTERED_NAMES
23+
24+
25+
def filter_names(names):
26+
return (name for name in names if is_valid_name(name))
27+
28+
29+
def filter_attributes(attributes):
30+
return {name: value for name, value in attributes.items() if is_valid_name(name)}
31+
32+
33+
def get_type_name(obj):
34+
type_ = type(obj)
35+
try:
36+
return type_.__qualname__
37+
except AttributeError:
38+
return type_.__name__
39+
40+
41+
def is_class(t):
42+
return isinstance(t, type)
43+
44+
45+
def is_callable(obj):
46+
return hasattr(obj, '__call__')
47+
48+
49+
def write_function(file, name, indent):
50+
file.write(indent)
51+
# TODO: ideally methods functions should be handled differently, like have self
52+
# but we don't know arguments and if it's staticmethod or classmethod anyway
53+
file.write("def {}(*args, **kwargs) -> 'typing.Any': ...\n".format(name))
54+
55+
56+
def write_variable(file, name, value, indent):
57+
type_name = get_type_name(value)
58+
file.write(indent)
59+
60+
if isinstance(value, property) or value is None or type_name in DESCRIPTORS:
61+
variable_type = 'typing.Any'
62+
else:
63+
variable_type = '{}'.format(type_name)
64+
65+
file.write(
66+
"{}: '{}'\n".format(name, variable_type)
67+
)
68+
69+
70+
def write_class(file, name, value, indent):
71+
file.write(indent)
72+
file.write("class {}:\n".format(name))
73+
attributes = filter_attributes(vars(value))
74+
75+
if not attributes:
76+
file.write(indent)
77+
file.write(" pass\n")
78+
else:
79+
for attribute_name, attribute_value in attributes.items():
80+
write_object(file, attribute_name, attribute_value, indent + ' ')
81+
82+
file.write('\n')
83+
file.write('\n')
84+
85+
86+
def write_object(file, name, value, indent):
87+
# TODO: https://github.com/20tab/UnrealEnginePython/issues/394 ESlateEnums contains invalid attributes
88+
if not is_valid_name(name):
89+
return
90+
91+
if is_class(value):
92+
write_class(file, name, value, indent)
93+
elif is_callable(value):
94+
write_function(file, name, indent)
95+
else:
96+
write_variable(file, name, value, indent)
97+
98+
99+
def write_ue_classes(file, classes):
100+
for class_ in classes:
101+
file.write("class {}:\n".format(class_.get_name()))
102+
103+
attributes_count = 0
104+
105+
for property_ in filter_names(class_.properties()):
106+
file.write(" ")
107+
file.write("{}: 'typing.Any'\n".format(property_))
108+
attributes_count += 1
109+
110+
for function_ in filter_names(class_.functions()):
111+
file.write(" ")
112+
file.write("def {}(*args, **kwargs) -> 'typing.Any': pass\n".format(function_))
113+
attributes_count += 1
114+
115+
if attributes_count == 0:
116+
file.write(" ")
117+
file.write("pass\n")
118+
119+
file.write("\n")
120+
121+
122+
def generate_pyi_stubs(directory, include_reflection=False):
123+
"""
124+
Generates pyi file. Note include_reflection has not been implemented yet.
125+
"""
126+
# include_reflection is still WIP and bit unusable, because:
127+
# * the result file is very large and PyCharm by default ignores such files
128+
# * it contains some invalid identifiers ('IsInAir?')
129+
if include_reflection:
130+
raise NotImplementedError
131+
132+
package_dir = os.path.join(directory, 'unreal_engine')
133+
os.mkdir(package_dir)
134+
135+
init_path = os.path.join(package_dir, '__init__.pyi')
136+
with open(init_path, mode='w') as file:
137+
file.write('import typing')
138+
file.write('\n\n')
139+
140+
ue_public = vars(ue)
141+
for name, value in filter_attributes(ue_public).items():
142+
write_object(file, name, value, '')
143+
file.write('\n')
144+
145+
if include_reflection:
146+
classes_path = os.path.join(package_dir, 'classes.pyi')
147+
with open(classes_path, mode='w') as file:
148+
write_ue_classes(file, ue.all_classes())
149+
150+
151+
if __name__ == '__main__':
152+
generate_pyi_stubs(sys.argv[1])

0 commit comments

Comments
 (0)