File: module_redeclarator.patch

package info (click to toggle)
renderdoc 1.24%2Bdfsg-1%2Bdeb12u1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 105,156 kB
  • sloc: cpp: 759,405; ansic: 309,460; python: 26,606; xml: 22,599; java: 11,365; cs: 7,181; makefile: 6,707; yacc: 5,682; ruby: 4,648; perl: 3,461; sh: 2,354; php: 2,119; lisp: 1,835; javascript: 1,524; tcl: 1,068; ml: 747
file content (451 lines) | stat: -rw-r--r-- 22,265 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
--- "/c/Program Files/JetBrains/PyCharm Community Edition 2020.3.2/plugins/python-ce/helpers/generator3/module_redeclarator.py"	2020-12-30 17:08:24.000000000 +0000
+++ pycharm_helpers/plugins/python-ce/helpers/generator3/module_redeclarator.py	2021-01-27 14:13:18.876000000 +0000
@@ -2,6 +2,15 @@
 from generator3.util_methods import *
 from generator3.util_methods import get_relative_path_by_qname
 from generator3.docstring_parsing import *
+import re
+import enum
+
+# Add patterns for identifying declarations we care about
+RTYPE_PATTERN = re.compile(r"[:@]rtype:\s*(.*)")
+TYPE_PATTERN = re.compile(r"[:@]type\s*:\s*(.*)")
+PARAM_PATTERN = re.compile(r"[:@]param\s+([^:]*)\s+([^: ]*):")
+NAMESPACE_PATTERN = re.compile(r"([^[\]]+\.)*([a-zA-Z][a-zA-Z0-9_]+)")
+DEFAULTS = re.compile(r"(=.*)")
 
 
 class emptylistdict(dict):
@@ -79,6 +88,8 @@
         # we write things into buffers out-of-order
         self.header_buf = Buf(self)
         self.imports_buf = Buf(self)
+        # each class gets its own list of dependency imports, and so do functions
+        self.func_imports_buf = Buf(self)
         self.functions_buf = Buf(self)
         self.classes_buf = Buf(self)
         self.classes_buffs = list()
@@ -94,6 +105,8 @@
         self.doing_builtins = doing_builtins
         self.ret_type_cache = {}
         self.used_imports = emptylistdict() # qual_mod_name -> [imported_names,..]: actually used imported names
+        # if we use Typing hints, import some things for it
+        self.used_typing = False
 
     def _initializeQApp4(self):
         try:  # QtGui should be imported _before_ QtCore package.
@@ -129,19 +142,23 @@
         if self.split_modules:
             last_pkg_dir = build_pkg_structure(self.cache_dir, self.qname)
             with fopen(os.path.join(last_pkg_dir, "__init__.py"), "w") as init:
-                for buf in (self.header_buf, self.imports_buf, self.functions_buf, self.classes_buf):
+                for buf in (self.header_buf, self.imports_buf, self.classes_buf):
                     buf.flush(init)
 
                 data = ""
-                for buf in self.classes_buffs:
+                for (buf,imports) in self.classes_buffs:
                     with fopen(os.path.join(last_pkg_dir, buf.name) + '.py', "w") as dummy:
                         self.header_buf.flush(dummy)
                         self.imports_buf.flush(dummy)
+                        imports.flush(dummy)
                         buf.flush(dummy)
                         data += self.create_local_import(buf.name)
 
                 init.write(data)
-                self.footer_buf.flush(init)
+
+                # Write out functions last so they can reference the classes imported above
+                for buf in (self.func_imports_buf, self.functions_buf, self.footer_buf):
+                    buf.flush(init)
         else:
             last_pkg_dir = build_pkg_structure(self.cache_dir, '.'.join(qname_parts[:-1]))
             # In some rare cases submodules of a binary might have been generated earlier than the module
@@ -155,13 +172,16 @@
             else:
                 skeleton_path = os.path.join(last_pkg_dir, qname_parts[-1] + '.py')
             with fopen(skeleton_path, "w") as mod:
-                for buf in (self.header_buf, self.imports_buf, self.functions_buf, self.classes_buf):
+                for buf in (self.header_buf, self.imports_buf, self.classes_buf):
                     buf.flush(mod)
 
-                for buf in self.classes_buffs:
+                for (buf,imports) in self.classes_buffs:
+                    imports.flush(mod)
                     buf.flush(mod)
 
-                self.footer_buf.flush(mod)
+                # Write out functions last so they can reference the classes imported above
+                for buf in (self.func_imports_buf, self.functions_buf, self.footer_buf):
+                    buf.flush(mod)
 
     # Some builtin classes effectively change __init__ signature without overriding it.
     # This callable serves as a placeholder to be replaced via REDEFINED_BUILTIN_SIGS
@@ -177,6 +197,50 @@
             data += "."
         data += name + " import " + name + "\n"
         return data
+    
+    # Parse a typing declaration and add the actual types references
+    def add_import_types(self, import_types, type_decl):
+        if '[' not in type_decl:
+            import_types.add(type_decl)
+            return
+
+        match = re.match('^[tT]uple\[(.*)\]$', type_decl)
+        if match:
+            for i in match.group(1).split(','):
+                self.add_import_types(import_types, i.strip())
+            return
+
+        match = re.match('^[lL]ist\[(.*)\]$', type_decl)
+        if match:
+            self.add_import_types(import_types, match.group(1).strip())
+            return
+
+        match = re.match('^[cC]allable\[\[(.*)\]\s*,\s*(.*)\]$', type_decl)
+        if match:
+            for i in match.group(1).split(','):
+                self.add_import_types(import_types, i.strip())
+            self.add_import_types(import_types, match.group(2).strip())
+            return
+
+    # For a given type that's referenced, figure out where to import it from and add
+    # to the list
+    def process_import_type(self, used_imports, p_modname, classname, import_type):
+        parent = None
+
+        if import_type == '...':
+            return
+
+        if import_type in dir(sys.modules[p_modname]):
+            if import_type != classname:
+                parent = '.'
+                child = import_type
+        elif '.' in import_type:
+            imp_split = import_type.split('.')
+            parent = '.'.join(imp_split)
+            child = imp_split[-1]
+
+        if parent is not None and child not in used_imports[parent]:
+            used_imports[parent].append(child)
 
     def find_imported_name(self, item):
         """
@@ -433,7 +497,43 @@
             if first_param:
                 seq.insert(0, first_param)
         seq = make_names_unique(seq)
-        return (seq, ret_type, doc_node)
+
+        import_types = set()
+        ret_hint = None
+
+        # Try to use :rtype: to add explicit type annotations to return type, since PyCharm
+        # doesn't parse rtype properly (at least with split modules)
+        if ret_type is None and ':rtype:' in signature_string:
+            result = RTYPE_PATTERN.search(signature_string)
+            if result is not None:
+                type_decl = result.group(1).strip()
+                if type_decl != class_name:
+                    self.add_import_types(import_types, type_decl)
+                    ret_hint = NAMESPACE_PATTERN.sub(r'\2', type_decl)
+                else:
+                    ret_hint = "'" + type_decl + "'"
+                self.used_typing = True
+
+        # Also use :param: to add necessary imports and fix up the parameters.
+        # PyCharm supports parsing :param: but the skeletons need the right imports
+        # and it fails for namespaced parameters (since the 'raw' parameter type
+        # foo.bar refers to a generated submodule called bar with the skeleton bar
+        # inside.
+        for p in PARAM_PATTERN.findall(signature_string):
+            type_decl = p[0]
+            if type_decl != class_name:
+                self.add_import_types(import_types, type_decl)
+            try:
+                defaults_match = [DEFAULTS.search(s) for s in seq]
+                idx = [DEFAULTS.sub('', s) for s in seq].index(p[1])
+                seq[idx] = '{}: {}'.format(p[1], NAMESPACE_PATTERN.sub(r'\2', p[0]))
+                if defaults_match[idx]:
+                    seq[idx] += defaults_match[idx].group(1)
+            except ValueError:
+                note("Warning: Unrecognised parameter {} in parameter list {}".format(p, seq))
+                pass
+
+        return (seq, ret_type, doc_node, list(import_types), ret_hint)
 
     def parse_func_doc(self, func_doc, func_id, func_name, class_name, deco=None, sip_generated=False):
         """
@@ -453,18 +553,23 @@
                     overloads.append(part[i + len(signature):])
             if len(overloads) > 1:
                 docstring_results = [self.restore_by_docstring(overload, class_name, deco) for overload in overloads]
+                import_types = []
                 ret_types = []
                 for result in docstring_results:
                     rt = result[1]
                     if rt and rt not in ret_types:
                         ret_types.append(rt)
+                    imps = result[3]
+                    for imp in imps:
+                        if imp and imp not in import_types:
+                            import_types.append(imp)
                 if ret_types:
                     ret_literal = " or ".join(ret_types)
                 else:
                     ret_literal = None
                 param_lists = [result[0] for result in docstring_results]
                 spec = build_signature(func_name, restore_parameters_for_overloads(param_lists))
-                return (spec, ret_literal, "restored from __doc__ with multiple overloads")
+                return (spec, ret_literal, "restored from __doc__ with multiple overloads", import_types)
 
         # find the first thing to look like a definition
         prefix_re = re.compile(r"\s*(?:(\w+)[ \t]+)?" + func_id + r"\s*\(") # "foo(..." or "int foo(..."
@@ -472,18 +577,21 @@
         # parse the part that looks right
         if match:
             ret_hint = match.group(1)
-            params, ret_literal, doc_note = self.restore_by_docstring(func_doc[match.end():], class_name, deco, ret_hint)
+            params, ret, doc_note, import_types, ret_hint = self.restore_by_docstring(func_doc[match.end():], class_name, deco, ret_hint)
             spec = func_name + flatten(params)
-            return (spec, ret_literal, doc_note)
+            # if we got a type hint, put it on the function declaration
+            if ret_hint:
+                spec = spec + ' -> ' + ret_hint
+            return (spec, ret, doc_note, import_types)
         else:
-            return (None, None, None)
+            return (None, None, None, [])
 
 
     def is_predefined_builtin(self, module_name, class_name, func_name):
         return self.doing_builtins and module_name == BUILTIN_MOD_NAME and (
             class_name, func_name) in PREDEFINED_BUILTIN_SIGS
 
-    def redo_function(self, out, p_func, p_name, indent, p_class=None, p_modname=None, classname=None, seen=None):
+    def redo_function(self, out, p_func, p_name, indent, p_class=None, p_modname=None, classname=None, seen=None, used_imports=None):
         """
         Restore function argument list as best we can.
         @param out output function of a Buf
@@ -531,6 +639,8 @@
                 deco = "classmethod"
             elif type(p_func).__name__.startswith('staticmethod'):
                 deco = "staticmethod"
+            elif str(descriptor).startswith('<staticmethod'):
+                deco = "staticmethod"
         if p_name == "__new__":
             deco = "staticmethod"
             deco_comment = " # known case of __new__"
@@ -580,11 +690,17 @@
             sig_restored = False
             action("parsing doc of func %r of class %r", p_name, p_class)
             if isinstance(funcdoc, STR_TYPES):
-                (spec, ret_literal, more_notes) = self.parse_func_doc(funcdoc, p_name, p_name, classname, deco,
+                (spec, ret_literal, more_notes, import_types) = self.parse_func_doc(funcdoc, p_name, p_name, classname, deco,
                                                                       sip_generated)
                 if spec is None and p_name == '__init__' and classname:
-                    (spec, ret_literal, more_notes) = self.parse_func_doc(funcdoc, classname, p_name, classname, deco,
+                    (spec, ret_literal, more_notes, import_types) = self.parse_func_doc(funcdoc, classname, p_name, classname, deco,
                                                                           sip_generated)
+
+                # if we have some imported types, process and add them to the used imports
+                if used_imports is not None:
+                    for imp in import_types:
+                        self.process_import_type(used_imports, p_modname, classname, imp)
+
                 sig_restored = spec is not None
                 if more_notes:
                     if sig_note:
@@ -613,7 +729,7 @@
             out(indent, p_name, " = ", deco, "(", p_name, ")", deco_comment)
         out(0, "") # empty line after each item
 
-    def redo_class(self, out, p_class, p_name, indent, p_modname=None, seen=None, inspect_dir=False):
+    def redo_class(self, out, p_class, p_name, indent, p_modname=None, seen=None, inspect_dir=False, used_imports=None):
         """
         Restores a class definition.
         @param out output function of a relevant buf
@@ -644,6 +760,11 @@
                     skipped_bases.append(str(base))
                     continue
                     # somehow import every base class
+                # Ignore the SwigPyObject internal class, which can't be added to KNOWN_FAKE_BASES
+                # because it is never directly accessible
+                if base.__name__ == 'SwigPyObject':
+                    skipped_bases.append(str(base))
+                    continue
                 base_name = base.__name__
                 qual_module_name = qualifier_of(base, skip_qualifiers)
                 got_existing_import = False
@@ -704,6 +825,11 @@
                         continue
                 except Exception:
                     continue
+
+            # Don't generate skeleton for internal enum properties
+            if isinstance(p_class, enum.EnumMeta) and (is_callable(item) or item_name[0] == '_'):
+                continue
+
             if is_callable(item) and not isinstance(item, type):
                 methods[item_name] = item
             elif is_property(item):
@@ -727,7 +853,7 @@
         for item_name in sorted_no_case(methods.keys()):
             item = methods[item_name]
             try:
-                self.redo_function(out, item, item_name, indent + 1, p_class, p_modname, classname=p_name, seen=seen_funcs)
+                self.redo_function(out, item, item_name, indent + 1, p_class, p_modname, classname=p_name, seen=seen_funcs, used_imports=used_imports)
             except:
                 handle_error_func(item_name, out)
                 #
@@ -760,9 +886,41 @@
                         out(indent + 1, '""":type: ', prop_type, '"""')
                     out(0, "")
             else:
-                out(indent + 1, item_name, " = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default")
+                # for properties with docstrings put them inside the getter so that PyCharm
+                # displays them
                 if prop_docstring:
-                    out(indent + 1, '"""', prop_docstring, '"""')
+                    ret = ''
+                    param = ''
+
+                    # Additionally if we see :type: in the docstring, add type hints
+                    result = TYPE_PATTERN.search(prop_docstring)
+                    if result is not None:
+                        type_decl = result.group(1).strip()
+                        import_types = set()
+                        if type_decl == p_name or type_decl == 'List[{}]'.format(p_name):
+                            type_decl = "'" + type_decl + "'"
+                        else:
+                            self.add_import_types(import_types, type_decl)
+                        for imp in import_types:
+                            self.process_import_type(used_imports, p_modname, p_name, imp)
+                        type_decl = type_decl.replace('...', '__ellipses__').split('.')[-1].replace('__ellipses__', '...')
+                        ret = ' -> {}'.format(type_decl)
+                        param = ': {}'.format(type_decl)
+                    
+                    out(indent + 1, "@property")
+                    out(indent + 1, "def {}(self){}:".format(item_name, ret))
+                    out(indent + 2, '"""', prop_docstring, '"""')
+                    out(indent + 2, 'pass')
+                    out(0, "")
+                    out(indent + 1, "@{}.setter".format(item_name))
+                    out(indent + 1, "def {}(self, value{}):".format(item_name, param))
+                    out(indent + 2, 'pass')
+                    out(0, "")
+
+                    self.used_typing = True
+                    continue
+
+                out(indent + 1, item_name, " = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default")
                 out(0, "")
         if properties:
             out(0, "") # empty line after the block
@@ -990,15 +1148,20 @@
             out(0, "# functions")
             out(0, "")
             seen_funcs = {}
+            func_used_imports = emptylistdict()
             for item_name in sorted_no_case(funcs.keys()):
                 if item_name in omitted_names:
                     out(0, "# definition of ", item_name, " omitted")
                     continue
                 item = funcs[item_name]
                 try:
-                    self.redo_function(out, item, item_name, 0, p_modname=p_name, seen=seen_funcs)
+                    self.redo_function(out, item, item_name, 0, p_modname=p_name, seen=seen_funcs, used_imports=func_used_imports)
                 except:
                     handle_error_func(item_name, out)
+            # don't import anything from . for functions, functions are emitted in __init__
+            # and all local classes are imported by the time they're defined
+            func_used_imports['.'] = []
+            self.output_import_froms(self.func_imports_buf.out, func_used_imports)
         else:
             self.functions_buf.out(0, "# no functions")
             #
@@ -1020,13 +1183,22 @@
             self.split_modules = self.mod_filename and len(cls_list) >= 30
             for item_name in [cls_item[0] for cls_item in cls_list]:
                 buf = ClassBuf(item_name, self)
-                self.classes_buffs.append(buf)
+                imports = ClassBuf(item_name + '_imports', self)
+                self.classes_buffs.append((buf,imports))
                 out = buf.out
                 if item_name in omitted_names:
                     out(0, "# definition of ", item_name, " omitted")
                     continue
                 item = classes[item_name]
-                self.redo_class(out, item, item_name, 0, p_modname=p_name, seen=seen_classes, inspect_dir=inspect_dir)
+                used_imports = emptylistdict()
+                self.redo_class(out, item, item_name, 0, p_modname=p_name, seen=seen_classes, inspect_dir=inspect_dir, used_imports=used_imports)
+                # if we don't have split modules we can't import dependencies, but we also
+                # have an ordering constraint - classes need to be declared after any classes
+                # they reference in type hints. This is broken either way though even with the
+                # return literals
+                if not self.split_modules:
+                    func_used_imports['.'] = []
+                self.output_import_froms(imports.out, used_imports)
                 self._defined[item_name] = True
                 out(0, "") # empty line after each item
 
@@ -1083,19 +1255,38 @@
             for value in values_to_add:
                 self.footer_buf.out(0, value)
                 # imports: last, because previous parts could alter used_imports or hidden_imports
-        self.output_import_froms()
+                
+        out = self.imports_buf.out
+        self.output_import_froms(out, self.used_imports)
+
+        if self.hidden_imports:
+            self.add_import_header_if_needed()
+            for mod_name in sorted_no_case(self.hidden_imports.keys()):
+                out(0, 'import ', mod_name, ' as ', self.hidden_imports[mod_name])
+            out(0, "") # empty line after group
+
+        if self.used_typing:
+            self.add_import_header_if_needed()
+            out(0, 'from typing import List, Tuple, Callable, Any')
+            out(0, "") # empty line after group
+
         if self.imports_buf.isEmpty():
-            self.imports_buf.out(0, "# no imports")
-        self.imports_buf.out(0, "") # empty line after imports
+            out(0, "# no imports")
+        out(0, "") # empty line after imports
 
-    def output_import_froms(self):
+    def output_import_froms(self, out, imports_list):
         """Mention all imported names known within the module, wrapping as per PEP."""
-        out = self.imports_buf.out
-        if self.used_imports:
+        if imports_list:
             self.add_import_header_if_needed()
-            for mod_name in sorted_no_case(self.used_imports.keys()):
-                import_names = self.used_imports[mod_name]
-                if import_names:
+            for mod_name in sorted_no_case(imports_list.keys()):
+                import_names = imports_list[mod_name]
+                if mod_name == '.':
+                    # if this is a local import, we need to treat it specially to import
+                    # the class inside the referenced module
+                    for n in import_names:
+                        out(0, "from .%s import %s" % (n, n)) # empty line after group
+                    out(0, "") # empty line after group
+                elif import_names:
                     self._defined[mod_name] = True
                     right_pos = 0 # tracks width of list to fold it at right margin
                     import_heading = "from % s import (" % mod_name
@@ -1127,10 +1318,4 @@
 
                     out(0, "") # empty line after group
 
-        if self.hidden_imports:
-            self.add_import_header_if_needed()
-            for mod_name in sorted_no_case(self.hidden_imports.keys()):
-                out(0, 'import ', mod_name, ' as ', self.hidden_imports[mod_name])
-            out(0, "") # empty line after group
-