1#!/usr/bin/env python
2"""Find the functions in a module missing type annotations.
3
4To use it run
5
6./functions_missing_types.py <module>
7
8and it will print out a list of functions in the module that don't
9have types.
10
11"""
12import argparse
13import ast
14import importlib
15import os
16
17NUMPY_ROOT = os.path.dirname(os.path.join(
18    os.path.abspath(__file__), "..",
19))
20
21# Technically "public" functions (they don't start with an underscore)
22# that we don't want to include.
23EXCLUDE_LIST = {
24    "numpy": {
25        # Stdlib modules in the namespace by accident
26        "absolute_import",
27        "division",
28        "print_function",
29        "warnings",
30        "sys",
31        "os",
32        "math",
33        # Accidentally public, deprecated, or shouldn't be used
34        "Tester",
35        "alen",
36        "add_docstring",
37        "add_newdoc",
38        "add_newdoc_ufunc",
39        "core",
40        "compat",
41        "fastCopyAndTranspose",
42        "get_array_wrap",
43        "int_asbuffer",
44        "oldnumeric",
45        "safe_eval",
46        "set_numeric_ops",
47        "test",
48        # Builtins
49        "bool",
50        "complex",
51        "float",
52        "int",
53        "long",
54        "object",
55        "str",
56        "unicode",
57        # More standard names should be preferred
58        "alltrue",  # all
59        "sometrue",  # any
60    }
61}
62
63
64class FindAttributes(ast.NodeVisitor):
65    """Find top-level attributes/functions/classes in stubs files.
66
67    Do this by walking the stubs ast. See e.g.
68
69    https://greentreesnakes.readthedocs.io/en/latest/index.html
70
71    for more information on working with Python's ast.
72
73    """
74
75    def __init__(self):
76        self.attributes = set()
77
78    def visit_FunctionDef(self, node):
79        if node.name == "__getattr__":
80            # Not really a module member.
81            return
82        self.attributes.add(node.name)
83        # Do not call self.generic_visit; we are only interested in
84        # top-level functions.
85        return
86
87    def visit_ClassDef(self, node):
88        if not node.name.startswith("_"):
89            self.attributes.add(node.name)
90        return
91
92    def visit_AnnAssign(self, node):
93        self.attributes.add(node.target.id)
94
95
96def find_missing(module_name):
97    module_path = os.path.join(
98        NUMPY_ROOT,
99        module_name.replace(".", os.sep),
100        "__init__.pyi",
101    )
102
103    module = importlib.import_module(module_name)
104    module_attributes = {
105        attribute for attribute in dir(module) if not attribute.startswith("_")
106    }
107
108    if os.path.isfile(module_path):
109        with open(module_path) as f:
110            tree = ast.parse(f.read())
111        ast_visitor = FindAttributes()
112        ast_visitor.visit(tree)
113        stubs_attributes = ast_visitor.attributes
114    else:
115        # No stubs for this module yet.
116        stubs_attributes = set()
117
118    exclude_list = EXCLUDE_LIST.get(module_name, set())
119
120    missing = module_attributes - stubs_attributes - exclude_list
121    print("\n".join(sorted(missing)))
122
123
124def main():
125    parser = argparse.ArgumentParser()
126    parser.add_argument("module")
127    args = parser.parse_args()
128
129    find_missing(args.module)
130
131
132if __name__ == "__main__":
133    main()
134