Spaces:
Sleeping
Sleeping
| """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 | |
| 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 | |