From 51a08162e2aceada33cc27e08940a871198b0b66 Mon Sep 17 00:00:00 2001 From: Alexander Quezada <101373246+irisfield@users.noreply.github.com> Date: Sat, 7 Nov 2026 01:21:47 -0700 Subject: [PATCH] fix(apps): import top-level project files under their module name during discovery --- runpod/apps/discovery.py | 31 +++++++++++++++++++++++++++++-- tests/test_apps/test_discovery.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/runpod/apps/discovery.py b/runpod/apps/discovery.py index 2287e5ede..a99c31b99 100644 --- a/runpod/apps/discovery.py +++ b/runpod/apps/discovery.py @@ -7,6 +7,7 @@ import importlib import importlib.util +import keyword import logging import os import sys @@ -19,6 +20,8 @@ log = logging.getLogger(__name__) +_SYNTHETIC_PREFIX = "_runpod_discovered_" + _SKIP_DIRS = { ".git", ".venv", @@ -57,7 +60,31 @@ def _module_name(path: Path) -> str: parent = parent.parent if parent != path.parent: return ".".join(parts) - return f"_runpod_discovered_{path.stem}_{abs(hash(str(path)))}" + if _imports_as_stem(path): + # a sibling's `from app import app` must reuse this module, or it + # re-runs the file and creates a second App with the same name + return path.stem + return f"{_SYNTHETIC_PREFIX}{path.stem}_{abs(hash(str(path)))}" + + +def _imports_as_stem(path: Path) -> bool: + """whether `import ` resolves to this top-level file.""" + name = path.stem + if not name.isidentifier() or keyword.iskeyword(name): + return False + existing = sys.modules.get(name) + if existing is not None: + source = getattr(existing, "__file__", None) + return bool(source) and Path(source).resolve() == path + try: + spec = importlib.util.find_spec(name) + except (ImportError, ValueError): + return False + return ( + spec is not None + and spec.origin is not None + and Path(spec.origin).resolve() == path + ) def _rollback_modules(before: set, root: Path) -> None: @@ -85,7 +112,7 @@ def _import_module(path: Path) -> None: module_name = _module_name(path) previous_module = sys.modules.get(module_name) try: - if (path.parent / "__init__.py").is_file(): + if not module_name.startswith(_SYNTHETIC_PREFIX): module = importlib.import_module(module_name) if Path(module.__file__).resolve() != path: raise DiscoveryError( diff --git a/tests/test_apps/test_discovery.py b/tests/test_apps/test_discovery.py index e133e4dec..e387202e3 100644 --- a/tests/test_apps/test_discovery.py +++ b/tests/test_apps/test_discovery.py @@ -95,6 +95,34 @@ def test_package_relative_imports_preserve_local_handle_identity(tmp_path, singl assert module.double.local(3) == 3 * factor +@pytest.mark.parametrize("worker_file", ["workers.py", "a_workers.py"]) +def test_flat_project_sibling_import_shares_app(tmp_path, worker_file): + (tmp_path / "app.py").write_text("from runpod import App\napp = App('flat')\n") + (tmp_path / worker_file).write_text( + "from app import app\n" + "@app.queue(name='embed', cpu='cpu3c-1-2')\n" + "def embed(value):\n" + " return value\n" + ) + + apps = discover_apps(tmp_path) + + assert [app.name for app in apps] == ["flat"] + assert list(apps[0].resources) == ["embed"] + assert get_registered_apps() == apps + + +def test_flat_module_named_like_installed_module_keeps_installed_module(tmp_path): + import json + + (tmp_path / "json.py").write_text("from runpod import App\napp = App('shadow')\n") + + apps = discover_apps(tmp_path) + + assert [app.name for app in apps] == ["shadow"] + assert sys.modules["json"] is json + + def test_discovery_restores_existing_invocation_guard(tmp_path, monkeypatch): target = tmp_path / "main.py" target.write_text("raise RuntimeError('broken')\n")