From e22039e5241ee6856e6d893851f8af8304736dfe Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 11:28:29 +0000 Subject: [PATCH 1/2] Skip the base-image placeholder src/ during entrypoint auto-detection The apify/actor-python base image ships a placeholder src/ package (its default CMD is `python -m src`) that prints "replace this file with your actual application code" - it exists in every derived image, so an Actor whose code lives elsewhere made auto-detection pick and debug the dummy. Detection now recognizes the placeholder by its marker text and skips it (falling through to main.py etc., or failing with guidance to pass the entry explicitly), and the CLI always logs which entrypoint it resolved so a wrong pick is visible in the run log. Version bumped to 0.1.1. Verified: unit scenarios for placeholder-only, placeholder-plus-real-code and real-src layouts, plus the full DAP bridge suite. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0194oFPjsTxztgsPhyB4Wq7G --- python/README.md | 4 +++ python/pyproject.toml | 2 +- python/src/actor_debugger/__init__.py | 2 +- python/src/actor_debugger/cli.py | 37 +++++++++++++++++++++++---- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/python/README.md b/python/README.md index 9f06ab5..efc5461 100644 --- a/python/README.md +++ b/python/README.md @@ -49,6 +49,10 @@ 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`. +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 — auto-detection recognizes and +skips it, and the run log always prints which entrypoint was resolved. If your Actor's entry has +a non-standard name, pass it explicitly (`-m mypackage` or `path/to/entry.py`). ## 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..87ddcdc 100644 --- a/python/src/actor_debugger/cli.py +++ b/python/src/actor_debugger/cli.py @@ -30,6 +30,23 @@ 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 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,14 +56,22 @@ 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])] + skipped_placeholders = [] # 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"): - return ["-m", "src.main"] + for module, files in (("src", ("src/__main__.py",)), ("src.main", ("src/main.py",))): + if all(os.path.isfile(f) for f in files): + if _is_placeholder("src/__main__.py", "src/main.py"): + skipped_placeholders.append(f"-m {module}") + break # both src candidates are the same placeholder package + return ["-m", module] for candidate in ("main.py", "__main__.py", "app.py"): if os.path.isfile(candidate): + if _is_placeholder(candidate): + skipped_placeholders.append(candidate) + continue return [os.path.abspath(candidate)] + for skipped in skipped_placeholders: + log(f"ignored {skipped}: it is the apify/actor-python base-image placeholder, not your code.") return None @@ -100,9 +125,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, From daea82a3e4a587f922c2173968d8b0bb0ff3875e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 11:33:21 +0000 Subject: [PATCH 2/2] Detect entrypoints across all Apify and Crawlee Python templates Auto-detection now scans for runnable packages by shape (any top-level directory with __main__.py) instead of assuming src/. This covers the current Apify templates (my_actor/, CMD python -m my_actor - including python-uv), the older src/ layout, and Crawlee-generated projects, whose package is named after the project (their Dockerfiles always end with CMD python -m ). The base-image placeholder src/ is still recognized by content and skipped; with several real candidates, src and my_actor are preferred, otherwise the log lists them and asks for an explicit -m. Ten layout scenarios covered by tests; bridge suite re-passes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0194oFPjsTxztgsPhyB4Wq7G --- python/README.md | 16 +++++++---- python/src/actor_debugger/cli.py | 49 +++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/python/README.md b/python/README.md index efc5461..02e038c 100644 --- a/python/README.md +++ b/python/README.md @@ -47,12 +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`. -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 — auto-detection recognizes and -skips it, and the run log always prints which entrypoint was resolved. If your Actor's entry has -a non-standard name, pass it explicitly (`-m mypackage` or `path/to/entry.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/src/actor_debugger/cli.py b/python/src/actor_debugger/cli.py index 87ddcdc..6884918 100644 --- a/python/src/actor_debugger/cli.py +++ b/python/src/actor_debugger/cli.py @@ -47,6 +47,27 @@ def _is_placeholder(*paths: str) -> bool: 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: @@ -56,22 +77,28 @@ 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])] - skipped_placeholders = [] - # The Apify Python templates run `python3 -m src` (src/__main__.py + src/main.py). - for module, files in (("src", ("src/__main__.py",)), ("src.main", ("src/main.py",))): - if all(os.path.isfile(f) for f in files): - if _is_placeholder("src/__main__.py", "src/main.py"): - skipped_placeholders.append(f"-m {module}") - break # both src candidates are the same placeholder package - return ["-m", module] + + 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): - skipped_placeholders.append(candidate) + log(f"ignored {candidate}: it is the apify/actor-python base-image placeholder, not your code.") continue return [os.path.abspath(candidate)] - for skipped in skipped_placeholders: - log(f"ignored {skipped}: it is the apify/actor-python base-image placeholder, not your code.") return None