3RNN/Lib/site-packages/namex/generate.py
2024-05-26 19:49:15 +02:00

161 lines
5.8 KiB
Python

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):
"""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}'.")
# Make list of all Python files (modules) to visit.
codebase_walk_entry_points = []
for root, _, files in os.walk(os.path.join(package, code_directory)):
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"]
if name == symbol.__name__:
init_file_lines.append(
f"from {symbol.__module__} import {name}"
)
else:
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:
init_file_lines.append(
f"from {'.'.join(path.split('/'))} import {symbol_metadata['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 []