Skip to content

Commit de2b826

Browse files
authored
feat(cli-import): make CLI import more robust (#960)
- Support `--app_dir` and add current dir to it by default - Improve error message - Update docs - Cleanup unrelated comments
1 parent 648a644 commit de2b826

File tree

3 files changed

+42
-16
lines changed

3 files changed

+42
-16
lines changed

docs/docs/core/cli.mdx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ Once CocoIndex is installed, you can invoke the CLI directly using the `cocoinde
1717
### APP_TARGET Format
1818

1919
The `APP_TARGET` can be:
20-
1. A **path to a Python file** defining your flows (e.g., `main.py`, `path/to/my_flows.py`).
21-
2. An **installed Python module name** that contains your flow definitions (e.g., `my_package.flows`).
20+
1. An **Python module name** that contains your flow definitions (e.g., `main`, `my_package.flows`).
21+
You can also use `--app-dir <path>` to specify the base directory to load the module from.
22+
23+
2. A **path to a Python file** defining your flows (e.g., `main.py`, `path/to/my_flows.py`).
24+
25+
The file will be loaded as a top-level Python module, e.g. relative imports will not work as its parent package is not defined (similar to how `python main.py` works).
26+
2227
3. For commands that operate on a *specific flow* (like `show`, `update`, `evaluate`), you can combine the application reference with a flow name:
2328
* `path/to/my_flows.py:MyFlow`
2429
* `my_package.flows:MyFlow`
@@ -44,6 +49,7 @@ If no file is found, only existing system environment variables are used.
4449
CocoIndex CLI supports the following global options:
4550

4651
* `--env-file <path>`: Load environment variables from a specified `.env` file. If not provided, `.env` in the current directory is loaded if it exists.
52+
* `--app-dir <path>`: Load apps from the specified directory. It will be treated as part of `PYTHONPATH`. Default to the current directory.
4753
* `--version`: Show the CocoIndex version and exit.
4854
* `--help`: Show the main help message and exit.
4955

python/cocoindex/cli.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import signal
77
import threading
8+
import sys
89
from types import FrameType
910
from typing import Any, Iterable
1011

@@ -19,9 +20,8 @@
1920
from .setup import flow_names_with_setup
2021
from .runtime import execution_context
2122
from .subprocess_exec import add_user_app
22-
from .user_app_loader import load_user_app
23+
from .user_app_loader import load_user_app, Error as UserAppLoaderError
2324

24-
# Create ServerSettings lazily upon first call, as environment variables may be loaded from files, etc.
2525
COCOINDEX_HOST = "https://cocoindex.io"
2626

2727

@@ -77,7 +77,16 @@ def _get_app_ref_from_specifier(
7777

7878

7979
def _load_user_app(app_target: str) -> None:
80-
load_user_app(app_target)
80+
if not app_target:
81+
raise click.ClickException("Application target not provided.")
82+
83+
try:
84+
load_user_app(app_target)
85+
except UserAppLoaderError as e:
86+
raise click.ClickException(
87+
f"Failed to load APP_TARGET '{app_target}': {e}"
88+
) from e
89+
8190
add_user_app(app_target)
8291

8392

@@ -99,7 +108,13 @@ def _initialize_cocoindex_in_process() -> None:
99108
default=None,
100109
show_default=False,
101110
)
102-
def cli(env_file: str | None = None) -> None:
111+
@click.option(
112+
"--app-dir",
113+
help="Load apps from the specified directory. Default to the current directory.",
114+
default="",
115+
show_default=True,
116+
)
117+
def cli(env_file: str | None = None, app_dir: str | None = "") -> None:
103118
"""
104119
CLI for Cocoindex.
105120
"""
@@ -109,6 +124,9 @@ def cli(env_file: str | None = None) -> None:
109124
loaded_env_path = os.path.abspath(dotenv_path)
110125
click.echo(f"Loaded environment variables from: {loaded_env_path}\n", err=True)
111126

127+
if app_dir is not None:
128+
sys.path.insert(0, app_dir)
129+
112130
try:
113131
_initialize_cocoindex_in_process()
114132
except Exception as e:

python/cocoindex/user_app_loader.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
import os
22
import sys
33
import importlib
4-
import click
54
import types
65

76

7+
class Error(Exception):
8+
"""
9+
Exception raised when a user app target is invalid or cannot be loaded.
10+
"""
11+
12+
pass
13+
14+
815
def load_user_app(app_target: str) -> types.ModuleType:
916
"""
1017
Loads the user's application, which can be a file path or an installed module name.
1118
Exits on failure.
1219
"""
13-
if not app_target:
14-
raise click.ClickException("Application target not provided.")
15-
1620
looks_like_path = os.sep in app_target or app_target.lower().endswith(".py")
1721

1822
if looks_like_path:
1923
if not os.path.isfile(app_target):
20-
raise click.ClickException(f"Application file path not found: {app_target}")
24+
raise Error(f"Application file path not found: {app_target}")
2125
app_path = os.path.abspath(app_target)
2226
app_dir = os.path.dirname(app_path)
2327
module_name = os.path.splitext(os.path.basename(app_path))[0]
@@ -35,7 +39,7 @@ def load_user_app(app_target: str) -> types.ModuleType:
3539
spec.loader.exec_module(module)
3640
return module
3741
except (ImportError, FileNotFoundError, PermissionError) as e:
38-
raise click.ClickException(f"Failed importing file '{app_path}': {e}")
42+
raise Error(f"Failed importing file '{app_path}': {e}") from e
3943
finally:
4044
if app_dir in sys.path and sys.path[0] == app_dir:
4145
sys.path.pop(0)
@@ -44,8 +48,6 @@ def load_user_app(app_target: str) -> types.ModuleType:
4448
try:
4549
return importlib.import_module(app_target)
4650
except ImportError as e:
47-
raise click.ClickException(f"Failed to load module '{app_target}': {e}")
51+
raise Error(f"Failed to load module '{app_target}': {e}") from e
4852
except Exception as e:
49-
raise click.ClickException(
50-
f"Unexpected error importing module '{app_target}': {e}"
51-
)
53+
raise Error(f"Unexpected error importing module '{app_target}': {e}") from e

0 commit comments

Comments
 (0)