File size: 6,036 Bytes
1f5470c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import sys
import importlib

INIT_FILE_HEADER = '''"""DO NOT EDIT.

This file was autogenerated. Do not edit it by hand,
since your modifications would be overwritten.
"""


'''


def generate_api_files(
        package,
        code_directory="src",
        verbose=False,
        target_directory=None,
        exclude_directories=()
):
    """Writes out API export `__init__.py` files.

    Given a codebase structured as such:

    ```
    package/
    ...src/
    ......__init__.py
    ......(Python files that use e.g. `@export_api(package="package", export_path="package.x.y.Z")`)
    ```

    this script generates `__init__.py` files within `package/`
    to export the public API described by the `@api_export` calls.

    Important notes:

    * Any existing `__init__.py` files in `package/` but outside of
        `package/code_directory/` may be overwritten.
    * This script must be run in an environment that includes
        all dependencies used by `package`. Make sure to install
        them before running the script.
    """
    if verbose:
        print(
            f"Generating files for package '{package}' "
            f"from sources found in '{package}/{code_directory}'."
        )

    if not os.path.exists(package):
        raise ValueError(f"No directory named '{package}'.")
    if not os.path.exists(os.path.join(package, code_directory)):
        raise ValueError(f"No directory named '{package}/{code_directory}'.")

    exclude_directories = [os.path.join(package, d) for d in exclude_directories]

    # Make list of all Python files (modules) to visit.
    codebase_walk_entry_points = []
    for root, dirs, files in os.walk(os.path.join(package, code_directory)):
        if root in exclude_directories:
            dirs.clear()
            continue
        for fname in files:
            if fname == "__init__.py":
                codebase_walk_entry_points.append(".".join(root.split("/")))
            elif fname.endswith(".py") and not fname.endswith("_test.py"):
                module_name = fname[:-3]
                codebase_walk_entry_points.append(
                    ".".join(root.split("/")) + "." + module_name
                )

    # Import all Python modules found in the code directory.
    sys.path.insert(0, os.getcwd())
    modules = []
    for entry_point in codebase_walk_entry_points:
        mod = importlib.import_module(entry_point, package=".")
        modules.append(mod)

    if verbose:
        print("Compiling list of symbols to export.")

    # Populate list of all symbols to register.
    all_symbols = set()
    for module in modules:
        for name in dir(module):
            symbol = getattr(module, name)
            if not hasattr(symbol, "_api_export_path"):
                continue
            if symbol._api_export_symbol_id != id(symbol):
                # This symbol is a non-exported subclass
                # of an exported symbol.
                continue
            if not all(
                [
                    path.startswith(package + ".")
                    for path in to_list(symbol._api_export_path)
                ]
            ):
                continue
            all_symbols.add(symbol)

    # Generate __init__ files content.
    init_files_content = {}
    for symbol in all_symbols:
        if verbose:
            print(f"...processing symbol '{symbol.__name__}'")
        for export_path in to_list(symbol._api_export_path):
            export_modules = export_path.split(".")
            if export_modules[0] == package and target_directory is not None:
                export_modules = [export_modules[0], target_directory] + export_modules[1:]
            export_name = export_modules[-1]
            parent_path = os.path.join(*export_modules[:-1])
            if parent_path not in init_files_content:
                init_files_content[parent_path] = []
            init_files_content[parent_path].append(
                {"symbol": symbol, "export_name": export_name}
            )
            for i in range(1, len(export_modules[:-1])):
                intermediate_path = os.path.join(*export_modules[:i])
                if intermediate_path not in init_files_content:
                    init_files_content[intermediate_path] = []
                init_files_content[intermediate_path].append(
                    {
                        "module": export_modules[i],
                        "location": ".".join(export_modules[:i]),
                    }
                )

    if verbose:
        print("Writing out API files.")

    # Go over init_files_content, make dirs,
    # create __init__.py file, populate file with public symbol imports.
    for path, contents in init_files_content.items():
        os.makedirs(path, exist_ok=True)
        init_file_lines = []
        modules_included = set()
        for symbol_metadata in contents:
            if "symbol" in symbol_metadata:
                symbol = symbol_metadata["symbol"]
                name = symbol_metadata["export_name"]
                init_file_lines.append(
                    f"from {symbol.__module__} import {symbol.__name__} as {name}"
                )
            elif "module" in symbol_metadata:
                if symbol_metadata["module"] not in modules_included:
                    module = symbol_metadata["module"]
                    init_file_lines.append(
                        f"from {'.'.join(path.split('/'))} import {module} as {module}"
                    )
                    modules_included.add(symbol_metadata["module"])

        init_path = os.path.join(path, "__init__.py")
        if verbose:
            print(f"...writing {init_path}")
        init_file_lines = sorted(init_file_lines)
        with open(init_path, "w") as f:
            contents = INIT_FILE_HEADER + "\n".join(init_file_lines) + "\n"
            f.write(contents)


def to_list(x):
    if isinstance(x, (list, tuple)):
        return list(x)
    elif isinstance(x, str):
        return [x]
    return []