Spaces:
Sleeping
Sleeping
File size: 5,411 Bytes
53dbcc1 | 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 | """Load Cyclopts App objects from Python scripts."""
import importlib.util
import sys
from contextlib import contextmanager
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cyclopts import App
from cyclopts.command_spec import CommandSpec
@contextmanager
def _suppress_app_execution():
"""Temporarily disable App.__call__ to prevent execution during module loading.
This context manager replaces App.__call__ with a no-op function, allowing
scripts that call app() at module level to be imported without executing.
"""
from cyclopts import App
original_call = App.__call__
def _dummy_call(self, *args, **kwargs):
"""No-op replacement for App.__call__ during module loading."""
return None
try:
App.__call__ = _dummy_call
yield
finally:
App.__call__ = original_call
def load_app_from_script(script: str | Path) -> tuple["App", str]:
"""Load a Cyclopts App object from a Python script.
Parameters
----------
script : str | Path
Python script path, optionally with ``':app_object'`` notation to specify
the :class:`App` object. If not specified, will search for :class:`App`
objects in the script's global namespace.
Returns
-------
tuple[App, str]
The loaded :class:`App` object and its name.
Raises
------
SystemExit
If the script cannot be loaded, no App objects are found, or multiple
App objects exist without specification.
"""
# Avoid circular import
from cyclopts import App
# Parse the script path and optional app object
app_name = None
script_str = str(script)
if ":" in script_str:
# Split on the last colon
script_path_str, potential_app_name = script_str.rsplit(":", 1)
# Only treat it as an app name if it looks like a Python identifier
# (no path separators), otherwise it may be part of a Windows path like C:\path\to\file.py
if potential_app_name and not any(sep in potential_app_name for sep in ["/", "\\"]):
# Looks like an app name
app_name = potential_app_name
script_path = Path(script_path_str)
else:
# It's part of the path (e.g., Windows drive letter)
script_path = Path(script)
else:
script_path = Path(script)
script_path = script_path.resolve()
if not script_path.exists():
print(f"Error: Script '{script_path}' not found.", file=sys.stderr)
sys.exit(1)
if not script_path.suffix == ".py":
print(f"Error: '{script_path}' is not a Python file.", file=sys.stderr)
sys.exit(1)
# Load the module
spec = importlib.util.spec_from_file_location("__cyclopts_doc_module", script_path)
if spec is None or spec.loader is None:
print(f"Error: Could not load module from '{script_path}'.", file=sys.stderr)
sys.exit(1)
module = importlib.util.module_from_spec(spec)
sys.modules["__cyclopts_doc_module"] = module
with _suppress_app_execution():
spec.loader.exec_module(module)
# Find the App object
if app_name:
# User specified the app object name
if not hasattr(module, app_name):
print(f"Error: No object named '{app_name}' found in '{script_path}'.", file=sys.stderr)
sys.exit(1)
app_obj = getattr(module, app_name)
if not isinstance(app_obj, App):
print(f"Error: '{app_name}' is not a Cyclopts App object.", file=sys.stderr)
sys.exit(1)
return app_obj, app_name
else:
# Heuristic: find App objects in the module's global namespace
app_objects = []
for name in dir(module):
if not name.startswith("_"): # Skip private/protected names
obj = getattr(module, name)
if isinstance(obj, App):
app_objects.append((name, obj))
if not app_objects:
print(f"Error: No Cyclopts App objects found in '{script_path}'.", file=sys.stderr)
sys.exit(1)
if len(app_objects) > 1:
# Filter out Apps that are registered as commands to other Apps
# Skip CommandSpec - those are lazy imports from other modules, not apps from this file
registered_apps = []
for _, app in app_objects:
if hasattr(app, "_commands"):
# Only include direct App references; CommandSpec entries can't point to apps in this file
registered_apps.extend(cmd for cmd in app._commands.values() if not isinstance(cmd, CommandSpec))
# Keep only Apps that are not registered to others
filtered_apps = [(name, app) for name, app in app_objects if app not in registered_apps]
if filtered_apps:
app_objects = filtered_apps
if len(app_objects) > 1:
names = ", ".join(name for name, _ in app_objects)
script_str = str(script) if isinstance(script, Path) else script
print(
f"Error: Multiple App objects found: {names}. Please specify one using '{script_str}:app_name'.",
file=sys.stderr,
)
sys.exit(1)
name, app_obj = app_objects[0]
return app_obj, name
|