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 []
|