Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <module>`/`<file.py>` 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 <module>` / `<file.py>` 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

Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
2 changes: 1 addition & 1 deletion python/src/actor_debugger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""actor-debugger: browser-based remote debugging for Apify Python Actors."""

__version__ = "0.1.0"
__version__ = "0.1.1"
64 changes: 59 additions & 5 deletions python/src/actor_debugger/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` 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:
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down