← Back to generate_inits documentation
generate_inits - Source Code¶
File: Distributed_Design_Optimizer/generate_inits.py
# Copyright (C) The DistributedDesignOptimizer Contributors
# Licensed under the GNU General Public License v3.0. See LICENSE file for details.
"""Script to auto-generate __init__.py files for the Distributed_Design_Optimizer package.
Uses AST parsing to discover public classes/functions from each package's direct .py
submodules ONLY (not from subpackages), then generates lazy imports via __getattr__
with a custom module __class__ to handle submodule name collisions.
Problem: When ClassName.py defines class ClassName, `from package import ClassName`
resolves the submodule file rather than calling __getattr__, so beartype sees a module
object instead of the class. The fix: a custom module subclass overrides
__getattribute__ to detect when a submodule would be returned for a name in
_LAZY_IMPORTS, and lazily resolves the class instead.
This means you can do:
from Distributed_Design_Optimizer.coordination.multilevelmethod import ALC, PC, LC
but NOT:
from Distributed_Design_Optimizer.coordination import ALC
Each level only re-exports what is defined in its own .py files.
Usage:
python -m Distributed_Design_Optimizer.generate_inits # generate all __init__.py files
python -m Distributed_Design_Optimizer.generate_inits --dry # preview without writing
Notes:
- Preserves everything outside the <AUTOGEN_INIT> tags (e.g. beartype_this_package())
- Creates missing __init__.py files with proper headers
- Safe to re-run at any time after adding/removing modules
- Uses lazy imports with module __class__ override to avoid both circular imports
and submodule name collisions with beartype
"""
import os
import sys
import re
import ast
import argparse
import warnings
# __file__ is inside Distributed_Design_Optimizer/, so go up one level for repo root
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, REPO_ROOT)
PACKAGE_ROOT = os.path.join(REPO_ROOT, "Distributed_Design_Optimizer")
USERFILES_ROOT = os.path.join(REPO_ROOT, "userfiles")
HEADER = """\
# Copyright (C) The DistributedDesignOptimizer Contributors
# Licensed under the GNU General Public License v3.0. See LICENSE file for details.
"""
AUTOGEN_OPEN = "# <AUTOGEN_INIT>"
AUTOGEN_CLOSE = "# </AUTOGEN_INIT>"
def find_all_packages(root):
"""Find all directories under root that are (or should be) Python packages.
Args:
root: Root directory path to search for packages.
"""
packages = []
for dirpath, dirnames, filenames in os.walk(root):
# Skip __pycache__, build, and hidden directories
dirnames[:] = [d for d in dirnames if not d.startswith(('.', '__')) and d != 'build']
# A directory is a package if it has .py files or subdirectories with .py files
has_py = any(f.endswith('.py') and f != '__init__.py' for f in filenames)
has_subpackages = any(
os.path.isfile(os.path.join(dirpath, d, '__init__.py'))
for d in dirnames
)
if has_py or has_subpackages:
packages.append(dirpath)
return packages
def ensure_init_with_tags(package_dir):
"""Ensure __init__.py exists and contains AUTOGEN_INIT tags.
Args:
package_dir: Path to the package directory.
"""
init_path = os.path.join(package_dir, "__init__.py")
if not os.path.exists(init_path):
# Create new __init__.py with header and tags
content = f"{HEADER}{AUTOGEN_OPEN}\n{AUTOGEN_CLOSE}\n"
with open(init_path, 'w') as f:
f.write(content)
warnings.warn(f"generate_inits: Created {os.path.relpath(init_path, REPO_ROOT)}")
return
# Read existing content
with open(init_path, 'r') as f:
content = f.read()
# If tags already present, nothing to do
if AUTOGEN_OPEN in content:
return
# Add tags at the end, preserving existing content
if not content.endswith('\n'):
content += '\n'
content += f"{AUTOGEN_OPEN}\n{AUTOGEN_CLOSE}\n"
with open(init_path, 'w') as f:
f.write(content)
warnings.warn(f"generate_inits: Added AUTOGEN tags to {os.path.relpath(init_path, REPO_ROOT)}")
def get_package_dotpath(package_dir):
"""Convert a filesystem path to a Python dotted module path.
Args:
package_dir: Filesystem path to the package directory.
"""
relpath = os.path.relpath(package_dir, REPO_ROOT)
return relpath.replace(os.sep, '.')
def get_module_names(package_dir):
"""Get names of direct .py submodules (without .py extension).
Args:
package_dir: Path to the package directory.
"""
modules = set()
for name in os.listdir(package_dir):
if name.endswith('.py') and name != '__init__.py' and name != 'generate_inits.py':
modules.add(name[:-3])
return modules
def get_public_names_from_module(module_path):
"""Extract public class/function/variable names from a .py file using AST.
Returns a list of public names defined at module level.
Args:
module_path: Filesystem path to the .py file.
"""
try:
with open(module_path, 'r', encoding='utf-8') as f:
source = f.read()
except (OSError, UnicodeDecodeError):
return []
try:
tree = ast.parse(source)
except SyntaxError:
return []
# Check for explicit __all__
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == '__all__':
if isinstance(node.value, (ast.List, ast.Tuple)):
names = []
for elt in node.value.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
names.append(elt.value)
if names:
return names
# No __all__, collect public top-level definitions
names = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
if not node.name.startswith('_'):
names.append(node.name)
elif isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and not target.id.startswith('_'):
names.append(target.id)
return names
def generate_lazy_init_content(package_dir):
"""Generate lazy import __init__.py content for a package.
Uses __getattr__ for lazy resolution (avoids circular imports) combined
with a custom module __class__ override (__getattribute__) that intercepts
submodule objects being returned for names in _LAZY_IMPORTS, resolving the
class instead. This handles the Python quirk where `from package import X`
returns the submodule when X.py exists, bypassing __getattr__.
Includes TYPE_CHECKING imports for Pylance/mypy static analysis.
Args:
package_dir: Path to the package directory.
"""
pkg_dotpath = get_package_dotpath(package_dir)
module_names = sorted(get_module_names(package_dir))
# Build mapping: public_name -> module_name
name_to_module = {}
all_names = []
for mod_name in module_names:
mod_path = os.path.join(package_dir, mod_name + '.py')
public_names = get_public_names_from_module(mod_path)
for name in public_names:
if name not in name_to_module:
name_to_module[name] = mod_name
all_names.append(name)
if not name_to_module:
return None
# Generate the lazy import code
lines = []
# Generate TYPE_CHECKING block for static analysis (Pylance / mypy).
# The lazy __getattr__ pattern defers imports to runtime for performance,
# but Pylance cannot statically resolve types from __getattr__-based exports.
# The TYPE_CHECKING guard provides type information to static analyzers while
# being completely invisible at runtime — it does not interfere with beartype
# or any other runtime type-checking, since TYPE_CHECKING is always False
# outside static analysis.
lines.append("from typing import TYPE_CHECKING")
lines.append("")
lines.append("# Static type-checking imports for Pylance / mypy.")
lines.append("# The lazy __getattr__ pattern below defers actual imports to runtime for performance,")
lines.append("# but Pylance cannot statically resolve types from __getattr__-based exports. This")
lines.append("# causes type hints (e.g. inherited methods) to appear unresolved in the IDE.")
lines.append("# The TYPE_CHECKING guard provides the type information Pylance needs while being")
lines.append("# completely invisible at runtime -- it does not interfere with beartype or any other")
lines.append("# runtime type-checking, since TYPE_CHECKING is always False outside static analysis.")
lines.append("# Note: the `import X as X` form is required per PEP 484 to signal an explicit")
lines.append("# re-export; without it, Pylance treats the import as private to this module.")
lines.append("if TYPE_CHECKING:")
# Use `import X as X` form to signal explicit re-export per PEP 484.
# Without the `as X`, Pylance treats the import as private to this module
# and won't re-export it to consumers (e.g. `from package import MyClass`).
for name in sorted(name_to_module.keys()):
mod = name_to_module[name]
lines.append(f" from {pkg_dotpath}.{mod} import {name} as {name}")
lines.append("")
# Build the _LAZY_IMPORTS mapping
lines.append("_LAZY_IMPORTS = {")
for name in sorted(name_to_module.keys()):
mod = name_to_module[name]
lines.append(f" '{name}': '{pkg_dotpath}.{mod}',")
lines.append("}")
lines.append("")
# Generate __getattr__ for lazy resolution (avoids circular imports)
#
# After defining __getattr__, we patch its co_filename to a frozen-module
# path so that debugpy classifies it as non-user code. This prevents
# the debugger's "Raised Exceptions" breakpoint from firing when Python
# probes dunder attributes like __wrapped__ (beartype) that don't exist.
lines.append("")
lines.append("def __getattr__(name):")
lines.append(" if name in _LAZY_IMPORTS:")
lines.append(" import importlib")
lines.append(" module = importlib.import_module(_LAZY_IMPORTS[name])")
lines.append(" value = getattr(module, name)")
lines.append(" if isinstance(value, type):")
lines.append(" value.__module__ = __name__ # help dill locate the class via the package")
lines.append(" globals()[name] = value # cache for subsequent access")
lines.append(" return value")
lines.append(" # Allow Python to resolve submodules (e.g. _plot_builder) not in _LAZY_IMPORTS")
lines.append(" if not name.startswith('__'):")
lines.append(" try:")
lines.append(" import importlib")
lines.append(" return importlib.import_module(f'{__name__}.{name}')")
lines.append(" except ImportError:")
lines.append(" pass")
lines.append(" raise AttributeError(f'module {__name__!r} has no attribute {name!r}')")
lines.append("")
lines.append("# Patch co_filename so debugpy treats __getattr__ as non-user code and does")
lines.append("# not break on AttributeError for dunder probes like __wrapped__.")
lines.append("__getattr__.__code__ = __getattr__.__code__.replace(")
lines.append(" co_filename='<frozen importlib._bootstrap>')")
lines.append("")
# Generate module __class__ override using __setattr__ to handle submodule
# name collisions WITHOUT __getattribute__.
#
# Problem: When ClassName.py defines class ClassName, `from package import ClassName`
# causes Python to import the .py file as a submodule, then call
# setattr(package, 'ClassName', <module>). After that, __getattr__ is never
# called because the attribute already exists as a module.
#
# Fix: Override __setattr__ to intercept when Python sets a submodule on
# the package for a name in _LAZY_IMPORTS. Instead of storing the module,
# resolve the class from it immediately.
#
# This avoids __getattribute__ entirely, which means no user-code frame
# is on the stack for dunder probes like __wrapped__, so the debugger's
# "Raised Exceptions" breakpoint is never triggered by those.
lines.append("import types as _types")
lines.append("import sys as _sys")
lines.append("")
lines.append("class _LazyClassModule(_types.ModuleType):")
lines.append(" _ModuleType = _types.ModuleType # bind before del")
lines.append(" def __setattr__(self, name, value):")
lines.append(" # When Python imports a submodule (e.g. ClassName.py), it calls")
lines.append(" # setattr(package, 'ClassName', <module>). If ClassName is in")
lines.append(" # _LAZY_IMPORTS, resolve the class from the module instead.")
lines.append(" if isinstance(value, _LazyClassModule._ModuleType):")
lines.append(" lazy = self.__dict__.get('_LAZY_IMPORTS', {})")
lines.append(" if name in lazy:")
lines.append(" try:")
lines.append(" resolved = getattr(value, name)")
lines.append(" if isinstance(resolved, type):")
lines.append(" resolved.__module__ = self.__name__")
lines.append(" super().__setattr__(name, resolved)")
lines.append(" return")
lines.append(" except (AttributeError, RecursionError):")
lines.append(" pass # fall through to normal setattr")
lines.append(" super().__setattr__(name, value)")
lines.append("")
lines.append("_sys.modules[__name__].__class__ = _LazyClassModule")
lines.append("del _types, _sys")
lines.append("")
# Generate __all__
all_str = ', '.join(f"'{n}'" for n in sorted(all_names))
lines.append(f"__all__ = [{all_str}]")
return '\n'.join(lines)
def update_init_file(init_path, autogen_content):
"""Replace the AUTOGEN section in an __init__.py file.
Args:
init_path: Path to the __init__.py file.
autogen_content: The new autogenerated content to insert.
"""
with open(init_path, 'r') as f:
content = f.read()
new_section = f"{AUTOGEN_OPEN}\n{autogen_content}\n{AUTOGEN_CLOSE}"
content = re.sub(
rf"{re.escape(AUTOGEN_OPEN)}.*?{re.escape(AUTOGEN_CLOSE)}",
new_section,
content,
flags=re.DOTALL,
)
with open(init_path, 'w') as f:
f.write(content)
def main(dry=False):
"""Generate lazy-import __init__.py files for all packages.
Args:
dry: If True, perform a dry run without writing files.
"""
packages = find_all_packages(PACKAGE_ROOT)
# Also include the root package itself
if PACKAGE_ROOT not in packages:
packages.insert(0, PACKAGE_ROOT)
for pkg_dir in packages:
ensure_init_with_tags(pkg_dir)
for pkg_dir in packages:
init_path = os.path.join(pkg_dir, "__init__.py")
# Skip the top-level package (handled separately)
if pkg_dir == PACKAGE_ROOT:
continue
autogen_content = generate_lazy_init_content(pkg_dir)
if autogen_content is None:
continue
if not dry:
update_init_file(init_path, autogen_content)
# The top-level package only re-exports its direct subpackages
top_init_path = os.path.join(PACKAGE_ROOT, "__init__.py")
direct_subpackages = [
d for d in os.listdir(PACKAGE_ROOT)
if os.path.isdir(os.path.join(PACKAGE_ROOT, d))
and not d.startswith(('.', '__'))
and d != 'build'
and os.path.isfile(os.path.join(PACKAGE_ROOT, d, '__init__.py'))
]
shallow_imports = "\n".join(
f"from Distributed_Design_Optimizer import {subpkg}"
for subpkg in sorted(direct_subpackages)
)
with open(top_init_path, 'r') as f:
content = f.read()
new_section = f"{AUTOGEN_OPEN}\n{shallow_imports}\n{AUTOGEN_CLOSE}\n"
content = re.sub(
rf"{re.escape(AUTOGEN_OPEN)}.*?{re.escape(AUTOGEN_CLOSE)}\n?",
new_section,
content,
flags=re.DOTALL,
)
if not dry:
with open(top_init_path, 'w') as f:
f.write(content)
userfiles_packages = find_all_packages(USERFILES_ROOT)
for pkg_dir in userfiles_packages:
init_path = os.path.join(pkg_dir, "__init__.py")
autogen_content = generate_lazy_init_content(pkg_dir)
if autogen_content is None:
continue
# Write clean file: header + AUTOGEN-wrapped content
full_content = f"{HEADER}{AUTOGEN_OPEN}\n{autogen_content}\n{AUTOGEN_CLOSE}\n"
if not dry:
with open(init_path, 'w') as f:
f.write(full_content)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Auto-generate __init__.py files")
parser.add_argument('--dry', action='store_true', help="Preview without writing")
args = parser.parse_args()
main(dry=args.dry)