diff --git a/python/README.md b/python/README.md index 9f06ab5..02e038c 100644 --- a/python/README.md +++ b/python/README.md @@ -47,8 +47,16 @@ CMD ["python3", "-m", "actor_debugger", "main.py"] # explicit file (`actor-debugger` also works as a console command; the `python3 -m` form is immune to PATH surprises in slim images.) -Entrypoint detection order: explicit `-m `/`` argument → `src/__main__.py` -(the Apify template's `python3 -m src`) → `src/main.py` → `main.py` / `__main__.py` / `app.py`. +Entrypoint detection: an explicit `-m ` / `` argument always wins. Otherwise the +launcher scans the working directory for **runnable packages** (top-level directories with a +`__main__.py`) — by shape, not by name — which covers every current Apify Python template +(`my_actor/`), older ones (`src/`), and Crawlee-generated projects, whose package is named after +your project. The `apify/actor-python` base image ships a placeholder `src/` package ("replace +this file with your actual application code") that exists in every derived image — it is +recognized by content and skipped. With several real runnable packages, `src` and `my_actor` are +preferred, otherwise the log lists the candidates and asks for an explicit `-m`. Flat layouts +fall back to `src/main.py` → `main.py` / `__main__.py` / `app.py`. The run log always prints +`entrypoint: …` so a wrong pick is immediately visible. ## How it works diff --git a/python/pyproject.toml b/python/pyproject.toml index 0bcba79..a0189c4 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "actor-debugger" -version = "0.1.0" +version = "0.1.1" description = "Drop-in remote debugging for Apify Python Actors: debugpy plus a browser debugger UI served over the run's container URL." readme = "README.md" license = { text = "Apache-2.0" } diff --git a/python/src/actor_debugger/__init__.py b/python/src/actor_debugger/__init__.py index ab3bee4..cad7c49 100644 --- a/python/src/actor_debugger/__init__.py +++ b/python/src/actor_debugger/__init__.py @@ -1,3 +1,3 @@ """actor-debugger: browser-based remote debugging for Apify Python Actors.""" -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/python/src/actor_debugger/cli.py b/python/src/actor_debugger/cli.py index 4dd8123..6884918 100644 --- a/python/src/actor_debugger/cli.py +++ b/python/src/actor_debugger/cli.py @@ -30,6 +30,44 @@ def log(message: str) -> None: print(f"{TAG} {message}", file=sys.stderr, flush=True) +# The apify/actor-python base image ships a placeholder src/ package (default CMD is +# `python -m src`) that only prints a "replace this file" warning - it exists in EVERY +# image built on that base, so auto-detection must never mistake it for the Actor's code. +_PLACEHOLDER_MARKER = b"set up your Docker image correctly" + + +def _is_placeholder(*paths: str) -> bool: + for path in paths: + try: + with open(path, "rb") as f: + if _PLACEHOLDER_MARKER in f.read(65536): + return True + except OSError: + continue + return False + + +def _runnable_packages() -> list[str]: + """Top-level packages that `python -m ` can run, excluding placeholders. + + Covers every current Apify Python template (`my_actor/`), older ones (`src/`), and + Crawlee-generated projects, whose package is named after the project - so the scan is + by shape (a directory with __main__.py), not by name. + """ + found = [] + for name in sorted(os.listdir(".")): + if name.startswith(".") or name in ("__pycache__", "actor_debugger", "node_modules", "storage"): + continue + main_file = os.path.join(name, "__main__.py") + if not os.path.isfile(main_file): + continue + if _is_placeholder(main_file, os.path.join(name, "main.py")): + log(f"ignored -m {name}: it is the apify/actor-python base-image placeholder, not your code.") + continue + found.append(name) + return found + + def resolve_entry(args: list[str]) -> list[str] | None: """Return the argv tail (["-m", "pkg"] or ["path.py"]) for the Actor's entrypoint.""" if "-m" in args: @@ -39,13 +77,27 @@ def resolve_entry(args: list[str]) -> list[str] | None: positional = [a for a in args if not a.startswith("-")] if positional: return [os.path.abspath(positional[0])] - # The Apify Python templates run `python3 -m src` (src/__main__.py + src/main.py). - if os.path.isfile("src/__main__.py"): - return ["-m", "src"] - if os.path.isfile("src/main.py"): + + packages = _runnable_packages() + if len(packages) == 1: + return ["-m", packages[0]] + if len(packages) > 1: + # Prefer the template conventions when a project carries several runnable packages. + for preferred in ("src", "my_actor"): + if preferred in packages: + log(f"multiple runnable packages found ({', '.join(packages)}); picking -m {preferred}.") + return ["-m", preferred] + log(f"multiple runnable packages found: {', '.join(packages)} - pass one explicitly:") + log(f' CMD ["python3", "-m", "actor_debugger", "-m", "{packages[0]}"]') + return None + + if os.path.isfile("src/main.py") and not _is_placeholder("src/main.py"): return ["-m", "src.main"] for candidate in ("main.py", "__main__.py", "app.py"): if os.path.isfile(candidate): + if _is_placeholder(candidate): + log(f"ignored {candidate}: it is the apify/actor-python base-image placeholder, not your code.") + continue return [os.path.abspath(candidate)] return None @@ -100,9 +152,11 @@ def main() -> None: brk = "--brk" in args entry = resolve_entry([a for a in args if a != "--brk"]) if not entry: - log("could not find an Actor entrypoint. Pass one explicitly:") + log("could not find an Actor entrypoint. Pass one explicitly, e.g.:") log(' CMD ["python3", "-m", "actor_debugger", "-m", "src"]') + log(' CMD ["python3", "-m", "actor_debugger", "server.py"]') sys.exit(1) + log(f"entrypoint: {' '.join(entry)}") debugpy_argv = [ sys.executable,