From 8c709288c2ce2754e95cf8838ca442ab0fd6d0d4 Mon Sep 17 00:00:00 2001 From: zeke <40004347+KAJdev@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:11:48 -0800 Subject: [PATCH] fix(apps): reject classes in queue and task decorators --- runpod/apps/handles.py | 6 +++++ tests/test_apps/test_app.py | 54 +++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/runpod/apps/handles.py b/runpod/apps/handles.py index e4e804f61..e70938414 100644 --- a/runpod/apps/handles.py +++ b/runpod/apps/handles.py @@ -54,6 +54,12 @@ def __init__( fn: Callable, spec: ResourceSpec, ): + if inspect.isclass(fn): + raise InvalidResourceError( + f"@app.{spec.kind.value} does not support class target '{fn.__name__}'. " + "Decorate a function and use @function.init for worker startup " + "initialization." + ) self._app = app self._fn = fn self.spec = spec diff --git a/tests/test_apps/test_app.py b/tests/test_apps/test_app.py index 882b9570f..8ed986518 100644 --- a/tests/test_apps/test_app.py +++ b/tests/test_apps/test_app.py @@ -66,6 +66,60 @@ def t(): assert t.spec.cpu == ["cpu5c-2-4"] +@pytest.mark.parametrize("decorator", ["queue", "task"]) +@pytest.mark.parametrize("name", [None, "inference"]) +def test_function_decorators_reject_classes_before_registration(decorator, name): + app = App("a") + + @app.queue() + def existing(): + return "ok" + + class Inference: + def __init__(self): + raise AssertionError("class targets must not be instantiated") + + def __call__(self, value): + return value + + with patch.object(app, "_register") as register: + with pytest.raises(InvalidResourceError) as exc: + getattr(app, decorator)(name=name)(Inference) + + register.assert_not_called() + assert app.resources == {"existing": existing} + assert f"@app.{decorator}" in str(exc.value) + assert "Inference" in str(exc.value) + assert "function" in str(exc.value) + assert "@function.init" in str(exc.value) + + +@pytest.mark.parametrize("decorator", ["queue", "task"]) +@pytest.mark.parametrize("is_async", [False, True]) +async def test_function_decorators_support_functions_with_startup_hooks( + decorator, is_async +): + app = App("a") + + def sync_handler(value): + return value * 2 + + async def async_handler(value): + return value * 2 + + handler = async_handler if is_async else sync_handler + function = getattr(app, decorator)()(handler) + + @function.init + def startup(): + raise AssertionError("startup hooks must not run during decoration or locally") + + assert app.resources == {handler.__name__: function} + assert function._init_fn is startup + result = function.local(21) + assert (await result if is_async else result) == 42 + + @pytest.mark.parametrize("cpu", [[], "", ["cpu3c-2-4", ""], [None]]) def test_cpu_selection_requires_nonempty_instance_ids(cpu): app = App("a")