From 362b207e5a4db03461de02f866332932bf54ae7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20Qui=C3=B1anola?= Date: Wed, 25 Mar 2026 14:09:24 -0700 Subject: [PATCH 1/4] fix: reject unknown kwargs in @remote to prevent typo bugs (AE-2313) **extra in @remote silently swallowed misspelled parameters like depndencies=["torch"], causing missing deps at runtime with opaque import errors. Now raises TypeError with "did you mean?" suggestions using difflib.get_close_matches. Also removes dead extra parameter from stub_resource chain and create_remote_class since no downstream consumer used it. --- .../test_class_execution_integration.py | 21 ++-- tests/unit/test_class_caching.py | 22 ++--- tests/unit/test_remote_unknown_kwargs.py | 96 +++++++++++++++++++ 3 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 tests/unit/test_remote_unknown_kwargs.py diff --git a/tests/integration/test_class_execution_integration.py b/tests/integration/test_class_execution_integration.py index e28afebc..4437df33 100644 --- a/tests/integration/test_class_execution_integration.py +++ b/tests/integration/test_class_execution_integration.py @@ -43,7 +43,6 @@ async def test_remote_decorator_on_class(self): resource_config=self.mock_resource_config, dependencies=self.dependencies, system_dependencies=self.system_dependencies, - timeout=60, ) class RemoteCalculator: def __init__(self, initial_value=0): @@ -206,7 +205,7 @@ def get_state(self): } RemoteCounter = create_remote_class( - StatefulCounter, self.mock_resource_config, [], [], True, {} + StatefulCounter, self.mock_resource_config, [], [], True ) counter = RemoteCounter(5) @@ -278,7 +277,7 @@ def get_completed_count(self): return self.tasks_completed RemoteWorker = create_remote_class( - AsyncWorker, self.mock_resource_config, [], [], True, {} + AsyncWorker, self.mock_resource_config, [], [], True ) worker = RemoteWorker() @@ -378,7 +377,6 @@ def process_with_config(self, input_data): ["scikit-learn", "pandas"], [], # system_dependencies True, # accelerate_downloads - {}, # extra ) model = RemoteModel( @@ -479,7 +477,7 @@ def get_service_info(self): api_keys = ["key1", "key2", "key3"] RemoteDataService = create_remote_class( - DataService, self.mock_resource_config, ["psycopg2"], [], True, {} + DataService, self.mock_resource_config, ["psycopg2"], [], True ) service = RemoteDataService(db_conn, cache_conf, api_keys=api_keys) @@ -550,7 +548,7 @@ def safe_method(self): return "This always works" RemoteErrorProneClass = create_remote_class( - ErrorProneClass, self.mock_resource_config, [], [], True, {} + ErrorProneClass, self.mock_resource_config, [], [], True ) error_instance = RemoteErrorProneClass(should_fail=True) @@ -586,7 +584,7 @@ def simple_method(self): return "hello" RemoteSimpleClass = create_remote_class( - SimpleClass, self.mock_resource_config, [], [], True, {} + SimpleClass, self.mock_resource_config, [], [], True ) instance = RemoteSimpleClass() @@ -622,7 +620,7 @@ def process_file(self): with tempfile.NamedTemporaryFile() as temp_file: RemoteUnserializableClass = create_remote_class( - UnserializableClass, self.mock_resource_config, [], [], True, {} + UnserializableClass, self.mock_resource_config, [], [], True ) # This should not fail during initialization (lazy serialization) @@ -674,7 +672,6 @@ def slow_method(self, duration): [], [], True, - {"timeout": 5}, # 5 second timeout ) instance = RemoteSlowClass() @@ -709,7 +706,6 @@ def test_invalid_class_type_error(self): [], [], True, - {}, ) # Test with function instead of class @@ -717,9 +713,7 @@ def not_a_class(): pass with pytest.raises(TypeError, match="Expected a class"): - create_remote_class( - not_a_class, self.mock_resource_config, [], [], True, {} - ) + create_remote_class(not_a_class, self.mock_resource_config, [], [], True) # Note: Testing class without __name__ is not practically possible # since Python classes always have __name__ attribute @@ -741,7 +735,6 @@ def use_dependency(self): ["nonexistent-package==999.999.999"], # Invalid package [], True, - {}, ) instance = RemoteDependentClass() diff --git a/tests/unit/test_class_caching.py b/tests/unit/test_class_caching.py index 16f2f4d4..989c308f 100644 --- a/tests/unit/test_class_caching.py +++ b/tests/unit/test_class_caching.py @@ -143,7 +143,7 @@ def __init__(self, value): self.value = value RemoteCacheTestClass = create_remote_class( - CacheTestClass, self.mock_resource_config, [], [], True, {} + CacheTestClass, self.mock_resource_config, [], [], True ) # First instance - should be cache miss @@ -177,7 +177,7 @@ def __init__(self, x, y=None): self.y = y RemoteMultiArgClass = create_remote_class( - MultiArgClass, self.mock_resource_config, [], [], True, {} + MultiArgClass, self.mock_resource_config, [], [], True ) # Different args should create different cache entries @@ -198,7 +198,7 @@ def __init__(self, file_handle, name="default"): self.name = name RemoteFileHandlerClass = create_remote_class( - FileHandlerClass, self.mock_resource_config, [], [], True, {} + FileHandlerClass, self.mock_resource_config, [], [], True ) with tempfile.NamedTemporaryFile() as temp_file: @@ -224,7 +224,7 @@ def __init__(self, value): self.value = value RemoteOptimizationTestClass = create_remote_class( - OptimizationTestClass, self.mock_resource_config, [], [], True, {} + OptimizationTestClass, self.mock_resource_config, [], [], True ) with patch( @@ -252,7 +252,7 @@ def get_value(self): return self.value RemoteConsistencyTestClass = create_remote_class( - ConsistencyTestClass, self.mock_resource_config, [], [], True, {} + ConsistencyTestClass, self.mock_resource_config, [], [], True ) instance1 = RemoteConsistencyTestClass(1) @@ -275,7 +275,7 @@ def __init__(self, file_handle): self.file_handle = file_handle RemoteUUIDFallbackClass = create_remote_class( - UUIDFallbackClass, self.mock_resource_config, [], [], True, {} + UUIDFallbackClass, self.mock_resource_config, [], [], True ) with ( @@ -301,7 +301,7 @@ def __init__(self, value): self.value = value RemoteMemoryTestClass = create_remote_class( - MemoryTestClass, self.mock_resource_config, [], [], True, {} + MemoryTestClass, self.mock_resource_config, [], [], True ) # Create many instances with same args - should only create one cache entry @@ -325,10 +325,10 @@ def __init__(self, value): self.value = value RemoteClassTypeA = create_remote_class( - ClassTypeA, self.mock_resource_config, [], [], True, {} + ClassTypeA, self.mock_resource_config, [], [], True ) RemoteClassTypeB = create_remote_class( - ClassTypeB, self.mock_resource_config, [], [], True, {} + ClassTypeB, self.mock_resource_config, [], [], True ) instanceA = RemoteClassTypeA(42) @@ -360,7 +360,7 @@ def __init__(self, value, config=None): ) RemoteStructureTestClass = create_remote_class( - StructureTestClass, resource_config, [], [], True, {} + StructureTestClass, resource_config, [], [], True ) instance = RemoteStructureTestClass(42, config={"key": "value"}) @@ -403,7 +403,7 @@ def __init__(self, data): ) RemoteSerializationTestClass = create_remote_class( - SerializationTestClass, resource_config, [], [], True, {} + SerializationTestClass, resource_config, [], [], True ) test_data = {"test": [1, 2, 3]} diff --git a/tests/unit/test_remote_unknown_kwargs.py b/tests/unit/test_remote_unknown_kwargs.py new file mode 100644 index 00000000..19d3957c --- /dev/null +++ b/tests/unit/test_remote_unknown_kwargs.py @@ -0,0 +1,96 @@ +"""Tests for @remote decorator rejecting unknown keyword arguments (AE-2313). + +The **extra catch-all in remote() silently swallows typos like +depndencies=["torch"], causing missing dependencies at runtime with +opaque import errors. These tests verify that unknown kwargs raise +TypeError with helpful "did you mean?" suggestions. +""" + +import warnings + +import pytest + +from runpod_flash.core.resources import ServerlessResource + + +@pytest.fixture +def resource(): + return ServerlessResource(name="test", gpu="A100", workers=1) + + +class TestRemoteRejectsUnknownKwargs: + """remote() must raise TypeError on unknown keyword arguments.""" + + def test_single_unknown_kwarg_raises_type_error(self, resource): + from runpod_flash.client import remote + + with pytest.raises(TypeError, match="unknown keyword argument"): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + remote(resource, bogus=True) + + def test_typo_of_dependencies_raises_with_suggestion(self, resource): + from runpod_flash.client import remote + + with pytest.raises(TypeError, match="depndencies.*Did you mean.*dependencies"): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + remote(resource, depndencies=["torch"]) + + def test_typo_of_system_dependencies_raises_with_suggestion(self, resource): + from runpod_flash.client import remote + + with pytest.raises( + TypeError, match="system_depndencies.*Did you mean.*system_dependencies" + ): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + remote(resource, system_depndencies=["git"]) + + def test_typo_of_accelerate_downloads_raises_with_suggestion(self, resource): + from runpod_flash.client import remote + + with pytest.raises( + TypeError, match="accelerate_download.*Did you mean.*accelerate_downloads" + ): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + remote(resource, accelerate_download=False) + + def test_multiple_unknown_kwargs_all_listed(self, resource): + from runpod_flash.client import remote + + with pytest.raises( + TypeError, + match="unknown keyword arguments.*foo.*bar|unknown keyword arguments.*bar.*foo", + ): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + remote(resource, foo=1, bar=2) + + def test_no_suggestion_for_completely_unrelated_kwarg(self, resource): + """Unknown kwargs with no close match should not include 'Did you mean?'.""" + from runpod_flash.client import remote + + with pytest.raises(TypeError, match="unknown keyword argument.*zzzzz"): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + remote(resource, zzzzz=True) + + def test_valid_kwargs_still_work(self, resource): + """All known parameters must still be accepted without error.""" + from runpod_flash.client import remote + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + decorator = remote( + resource, + dependencies=["torch"], + system_dependencies=["git"], + accelerate_downloads=False, + local=True, + method=None, + path=None, + ) + # local=True returns the function unwrapped, decorator should be callable + assert callable(decorator) From 215ab08dd70ce4d9356097e02b28c6a61578adf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20Qui=C3=B1anola?= Date: Wed, 25 Mar 2026 14:10:50 -0700 Subject: [PATCH 2/4] fix: remove **extra from remote/stub_resource/create_remote_class (AE-2313) Remove dead **extra parameter from the full chain: client.py remote(), stubs/registry.py stub_resource() and all registered implementations, and execute_class.py create_remote_class/RemoteClassWrapper. Update all test files to remove trailing extra/{} arguments. --- src/runpod_flash/client.py | 32 ++++++++++++++++++++++++---- src/runpod_flash/execute_class.py | 4 +--- src/runpod_flash/stubs/registry.py | 26 +++++++++++----------- tests/unit/test_execute_class.py | 17 ++------------- tests/unit/test_p1_gaps.py | 4 ++-- tests/unit/test_p2_gaps.py | 23 ++++++++++---------- tests/unit/test_p2_remaining_gaps.py | 3 --- tests/unit/test_regressions.py | 8 +++---- tests/unit/test_stub_registry.py | 2 +- 9 files changed, 62 insertions(+), 57 deletions(-) diff --git a/src/runpod_flash/client.py b/src/runpod_flash/client.py index 04d9ed64..982dc2f6 100644 --- a/src/runpod_flash/client.py +++ b/src/runpod_flash/client.py @@ -1,6 +1,7 @@ -import os +import difflib import inspect import logging +import os from functools import wraps from typing import Any, List, Optional @@ -95,6 +96,19 @@ async def _resolve_deployed_endpoint_id(func_name: str) -> Optional[str]: return None +def _reject_unknown_kwargs(extra: dict[str, Any], known: set[str]) -> None: + """Raise TypeError for unknown kwargs with 'did you mean?' suggestions.""" + names = sorted(extra) + parts: list[str] = [] + for name in names: + close = difflib.get_close_matches(name, known, n=1, cutoff=0.6) + hint = f" (Did you mean '{close[0]}'?)" if close else "" + parts.append(f"'{name}'{hint}") + + noun = "argument" if len(names) == 1 else "arguments" + raise TypeError(f"remote() got unknown keyword {noun}: {', '.join(parts)}") + + def remote( resource_config: ServerlessResource, dependencies: Optional[List[str]] = None, @@ -106,6 +120,18 @@ def remote( _internal: bool = False, **extra, ): + _KNOWN_KWARGS = { + "resource_config", + "dependencies", + "system_dependencies", + "accelerate_downloads", + "local", + "method", + "path", + "_internal", + } + if extra: + _reject_unknown_kwargs(extra, _KNOWN_KWARGS) """ .. deprecated:: Use :class:`runpod_flash.Endpoint` instead. @@ -142,7 +168,6 @@ def remote( Ignored for queue-based endpoints. Defaults to None. _internal (bool, optional): suppress deprecation warning when called from Endpoint internals. not part of the public API. Defaults to False. - extra (dict, optional): Additional parameters for the execution of the resource. Defaults to an empty dict. Returns: Callable: A decorator that wraps the target function, enabling remote execution with the specified @@ -268,7 +293,6 @@ def decorator(func_or_class): dependencies, system_dependencies, accelerate_downloads, - extra, ) wrapped_class.__remote_config__ = routing_config return wrapped_class @@ -287,7 +311,7 @@ async def wrapper(*args, **kwargs): resource_config ) - stub = stub_resource(remote_resource, **extra) + stub = stub_resource(remote_resource) return await stub( func_or_class, dependencies, diff --git a/src/runpod_flash/execute_class.py b/src/runpod_flash/execute_class.py index 1ef809d9..126ae8c4 100644 --- a/src/runpod_flash/execute_class.py +++ b/src/runpod_flash/execute_class.py @@ -190,7 +190,6 @@ def create_remote_class( dependencies: Optional[List[str]], system_dependencies: Optional[List[str]], accelerate_downloads: bool, - extra: dict, ): """ Create a remote class wrapper. @@ -211,7 +210,6 @@ def __init__(self, *args, **kwargs): self._dependencies = dependencies or [] self._system_dependencies = system_dependencies or [] self._accelerate_downloads = accelerate_downloads - self._extra = extra self._constructor_args = args self._constructor_kwargs = kwargs self._instance_id = ( @@ -235,7 +233,7 @@ async def _ensure_initialized(self): remote_resource = await resource_manager.get_or_deploy_resource( self._resource_config ) - self._stub = stub_resource(remote_resource, **self._extra) + self._stub = stub_resource(remote_resource) # Create the remote instance by calling a method (which will trigger instance creation) # We'll do this on first method call diff --git a/src/runpod_flash/stubs/registry.py b/src/runpod_flash/stubs/registry.py index 23a50fad..96963de0 100644 --- a/src/runpod_flash/stubs/registry.py +++ b/src/runpod_flash/stubs/registry.py @@ -19,14 +19,14 @@ @singledispatch -def stub_resource(resource, **extra): +def stub_resource(resource): async def fallback(*args, **kwargs): return {"error": f"Cannot stub {resource.__class__.__name__}."} return fallback -def _create_live_serverless_stub(resource, **extra): +def _create_live_serverless_stub(resource): """Create a live serverless stub for both LiveServerless and CpuLiveServerless.""" stub = LiveServerlessStub(resource) @@ -105,17 +105,17 @@ async def wrapped_class_method(request): @stub_resource.register(LiveServerless) -def _(resource, **extra): - return _create_live_serverless_stub(resource, **extra) +def _(resource): + return _create_live_serverless_stub(resource) @stub_resource.register(CpuLiveServerless) -def _(resource, **extra): - return _create_live_serverless_stub(resource, **extra) +def _(resource): + return _create_live_serverless_stub(resource) @stub_resource.register(ServerlessEndpoint) -def _(resource, **extra): +def _(resource): async def stubbed_resource( func, dependencies, @@ -132,14 +132,14 @@ async def stubbed_resource( stub = ServerlessEndpointStub(resource) payload = stub.prepare_payload(func, *args, **kwargs) - response = await stub.execute(payload, sync=extra.get("sync", False)) + response = await stub.execute(payload, sync=False) return stub.handle_response(response) return stubbed_resource @stub_resource.register(CpuServerlessEndpoint) -def _(resource, **extra): +def _(resource): async def stubbed_resource( func, dependencies, @@ -156,14 +156,14 @@ async def stubbed_resource( stub = ServerlessEndpointStub(resource) payload = stub.prepare_payload(func, *args, **kwargs) - response = await stub.execute(payload, sync=extra.get("sync", False)) + response = await stub.execute(payload, sync=False) return stub.handle_response(response) return stubbed_resource @stub_resource.register(LoadBalancerSlsResource) -def _(resource, **extra): +def _(resource): """Create stub for LoadBalancerSlsResource (HTTP-based execution).""" stub = LoadBalancerSlsStub(resource) @@ -188,7 +188,7 @@ async def stubbed_resource( @stub_resource.register(LiveLoadBalancer) -def _(resource, **extra): +def _(resource): """Create stub for LiveLoadBalancer (HTTP-based execution, local testing).""" stub = LoadBalancerSlsStub(resource) @@ -213,7 +213,7 @@ async def stubbed_resource( @stub_resource.register(CpuLiveLoadBalancer) -def _(resource, **extra): +def _(resource): """Create stub for CpuLiveLoadBalancer (HTTP-based execution, local testing).""" stub = LoadBalancerSlsStub(resource) diff --git a/tests/unit/test_execute_class.py b/tests/unit/test_execute_class.py index 6f499f47..5fde0cea 100644 --- a/tests/unit/test_execute_class.py +++ b/tests/unit/test_execute_class.py @@ -226,7 +226,6 @@ def setup_method(self): ) self.dependencies = ["numpy", "pandas"] self.system_dependencies = ["git"] - self.extra = {"timeout": 30} def test_create_remote_class_basic(self): """Test basic remote class creation.""" @@ -244,7 +243,6 @@ def get_value(self): self.dependencies, self.system_dependencies, True, - self.extra, ) # Should return a class @@ -265,7 +263,6 @@ def __init__(self, value, name="default"): self.dependencies, self.system_dependencies, True, - self.extra, ) instance = RemoteWrapper(42, name="test") @@ -274,7 +271,6 @@ def __init__(self, value, name="default"): assert instance._resource_config == self.mock_resource_config assert instance._dependencies == self.dependencies assert instance._system_dependencies == self.system_dependencies - assert instance._extra == self.extra assert instance._constructor_args == (42,) assert instance._constructor_kwargs == {"name": "test"} assert instance._instance_id.startswith("TestClass_") @@ -293,7 +289,6 @@ class TestClass: None, # dependencies None, # system_dependencies True, # accelerate_downloads - self.extra, ) instance = RemoteWrapper() @@ -316,7 +311,6 @@ class TestClass: self.dependencies, self.system_dependencies, True, - self.extra, ) instance = RemoteWrapper() @@ -352,7 +346,6 @@ class TestClass: self.dependencies, self.system_dependencies, True, - self.extra, ) instance = RemoteWrapper() @@ -393,7 +386,6 @@ class TestClass: self.dependencies, self.system_dependencies, True, - self.extra, ) instance = RemoteWrapper() @@ -424,7 +416,6 @@ def add(self, x, y=10): self.dependencies, self.system_dependencies, True, - self.extra, ) instance = RemoteWrapper(5) @@ -489,7 +480,6 @@ def method2(self): self.dependencies, self.system_dependencies, True, - self.extra, ) instance = RemoteWrapper() @@ -528,7 +518,7 @@ def simple_method(self): return "simple" RemoteWrapper = create_remote_class( - TestClass, self.mock_resource_config, [], [], True, {} + TestClass, self.mock_resource_config, [], [], True ) instance = RemoteWrapper() @@ -571,7 +561,6 @@ def test_method(self): self.dependencies, self.system_dependencies, True, - self.extra, ) instance = RemoteWrapper() @@ -596,7 +585,6 @@ class TestClass: self.dependencies, self.system_dependencies, True, - self.extra, ) instance1 = RemoteWrapper() @@ -642,7 +630,7 @@ def get_value(self): ) RemoteCalculator = create_remote_class( - CalculatorClass, resource_config, ["numpy"], [], True, {"timeout": 60} + CalculatorClass, resource_config, ["numpy"], [], True ) calculator = RemoteCalculator(10) @@ -693,7 +681,6 @@ def complex_method( [], # dependencies [], # system_dependencies True, # accelerate_downloads - {}, # extra ) instance = RemoteWrapper("test", extra_arg=True) diff --git a/tests/unit/test_p1_gaps.py b/tests/unit/test_p1_gaps.py index 2ca46f9f..ad487cf4 100644 --- a/tests/unit/test_p1_gaps.py +++ b/tests/unit/test_p1_gaps.py @@ -134,7 +134,7 @@ def _internal(self): return "secret" mock_resource = MagicMock() - Wrapper = create_remote_class(MyModel, mock_resource, [], [], True, {}) + Wrapper = create_remote_class(MyModel, mock_resource, [], [], True) instance = Wrapper() with pytest.raises(AttributeError, match="has no attribute '_internal'"): @@ -149,7 +149,7 @@ def predict(self, data): return data mock_resource = MagicMock() - Wrapper = create_remote_class(MyModel, mock_resource, [], [], True, {}) + Wrapper = create_remote_class(MyModel, mock_resource, [], [], True) instance = Wrapper() with pytest.raises(AttributeError): diff --git a/tests/unit/test_p2_gaps.py b/tests/unit/test_p2_gaps.py index 9fa42501..8184dd55 100644 --- a/tests/unit/test_p2_gaps.py +++ b/tests/unit/test_p2_gaps.py @@ -58,26 +58,25 @@ async def my_func(x): # --------------------------------------------------------------------------- -# REM-FN-010: Extra **kwargs forwarded to stub_resource() +# REM-FN-010: Unknown **kwargs raise TypeError (AE-2313) # --------------------------------------------------------------------------- -class TestExtraKwargsForwarded: - """@remote extra kwargs forwarded to stub_resource().""" +class TestUnknownKwargsRejected: + """@remote rejects unknown kwargs to prevent typo bugs.""" @patch.dict(os.environ, {}, clear=True) - def test_extra_kwargs_accepted(self): - """REM-FN-010: Extra kwargs do not raise at decoration time.""" + def test_unknown_kwargs_raise_type_error(self): + """REM-FN-010: Unknown kwargs raise TypeError at decoration time.""" + import warnings + from runpod_flash.client import remote from runpod_flash.core.resources import LiveServerless resource = LiveServerless(name="extra-kwargs") - # extra kwargs should be accepted without error - @remote(resource, custom_param="foo", another=42) - async def my_func(x): - return x - - assert hasattr(my_func, "__remote_config__") - assert callable(my_func) + with pytest.raises(TypeError, match="unknown keyword arguments"): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + remote(resource, custom_param="foo", another=42) # --------------------------------------------------------------------------- diff --git a/tests/unit/test_p2_remaining_gaps.py b/tests/unit/test_p2_remaining_gaps.py index 5cde7ba4..58dd07e6 100644 --- a/tests/unit/test_p2_remaining_gaps.py +++ b/tests/unit/test_p2_remaining_gaps.py @@ -463,7 +463,6 @@ def infer(self, x): dependencies=None, system_dependencies=None, accelerate_downloads=True, - extra={}, ) instance = WrapperClass() assert instance._class_type is TargetClass @@ -487,7 +486,6 @@ def run(self, payload: dict, temperature: float = 0.7) -> str: dependencies=None, system_dependencies=None, accelerate_downloads=True, - extra={}, ) instance = WrapperClass() @@ -512,7 +510,6 @@ class SharedClass: dependencies=None, system_dependencies=None, accelerate_downloads=True, - extra={}, ) inst1 = WrapperClass() inst2 = WrapperClass() diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 282ad420..010bd668 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -64,7 +64,7 @@ def predict(self, x): return x * 2 mock_resource = MagicMock() - RemoteWrapper = create_remote_class(HeavyModel, mock_resource, [], [], True, {}) + RemoteWrapper = create_remote_class(HeavyModel, mock_resource, [], [], True) # Instantiate the wrapper — original __init__ should NOT fire instance = RemoteWrapper() @@ -87,7 +87,7 @@ def run(self, data): return data mock_resource = MagicMock() - RemoteWrapper = create_remote_class(GPUWorker, mock_resource, [], [], True, {}) + RemoteWrapper = create_remote_class(GPUWorker, mock_resource, [], [], True) _ = RemoteWrapper() assert init_called is False @@ -142,7 +142,7 @@ def infer(self, data): return data mock_resource = MagicMock() - Wrapper = create_remote_class(ModelWorker, mock_resource, [], [], True, {}) + Wrapper = create_remote_class(ModelWorker, mock_resource, [], [], True) _ = Wrapper() # import_attempted stays False because RemoteClassWrapper.__init__ @@ -158,7 +158,7 @@ def process(self, x: int) -> int: return x * 2 mock_resource = MagicMock() - Wrapper = create_remote_class(MyClass, mock_resource, [], [], True, {}) + Wrapper = create_remote_class(MyClass, mock_resource, [], [], True) instance = Wrapper() assert instance._class_type is MyClass diff --git a/tests/unit/test_stub_registry.py b/tests/unit/test_stub_registry.py index 0cdb632d..a6cc42a6 100644 --- a/tests/unit/test_stub_registry.py +++ b/tests/unit/test_stub_registry.py @@ -46,7 +46,7 @@ async def test_fallback_returns_error_dict(self): @pytest.mark.asyncio async def test_fallback_accepts_any_args(self): """Fallback handler accepts arbitrary args/kwargs.""" - result = stub_resource(42, extra_kwarg="test") + result = stub_resource(42) assert callable(result) error = await result("arg1", key="val") assert isinstance(error, dict) From b7ec0568122ebcaef769e204845b9b71ebdaa292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20Qui=C3=B1anola?= Date: Wed, 25 Mar 2026 15:57:34 -0700 Subject: [PATCH 3/4] fix: derive known kwargs from remote() signature, restore docstring - Derive _REMOTE_KNOWN_KWARGS from inspect.signature(remote) so it stays in sync automatically when parameters are added/removed - Move validation after docstring so remote.__doc__ is preserved - Add comment explaining why **extra is retained in the signature --- src/runpod_flash/client.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/runpod_flash/client.py b/src/runpod_flash/client.py index 982dc2f6..05bfea8c 100644 --- a/src/runpod_flash/client.py +++ b/src/runpod_flash/client.py @@ -118,20 +118,10 @@ def remote( method: Optional[str] = None, path: Optional[str] = None, _internal: bool = False, + # **extra is retained (rather than removing it and relying on Python's own + # TypeError) so we can provide "did you mean?" suggestions for typos. **extra, ): - _KNOWN_KWARGS = { - "resource_config", - "dependencies", - "system_dependencies", - "accelerate_downloads", - "local", - "method", - "path", - "_internal", - } - if extra: - _reject_unknown_kwargs(extra, _KNOWN_KWARGS) """ .. deprecated:: Use :class:`runpod_flash.Endpoint` instead. @@ -205,6 +195,8 @@ async def my_test_function(data): pass ``` """ + if extra: + _reject_unknown_kwargs(extra, _REMOTE_KNOWN_KWARGS) if not _internal: import warnings @@ -326,3 +318,11 @@ async def wrapper(*args, **kwargs): return wrapper return decorator + + +# Derived from remote()'s signature so it stays in sync automatically. +_REMOTE_KNOWN_KWARGS = { + p.name + for p in inspect.signature(remote).parameters.values() + if p.kind != inspect.Parameter.VAR_KEYWORD +} From b0ffdf14c913239392b87f5adaeab9a3040cb21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20Qui=C3=B1anola?= Date: Wed, 25 Mar 2026 16:16:05 -0700 Subject: [PATCH 4/4] fix: sort known kwargs for deterministic difflib suggestions Pass sorted(known) to difflib.get_close_matches so "did you mean?" hints are deterministic regardless of set iteration order. --- src/runpod_flash/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runpod_flash/client.py b/src/runpod_flash/client.py index 05bfea8c..62d4e692 100644 --- a/src/runpod_flash/client.py +++ b/src/runpod_flash/client.py @@ -101,7 +101,7 @@ def _reject_unknown_kwargs(extra: dict[str, Any], known: set[str]) -> None: names = sorted(extra) parts: list[str] = [] for name in names: - close = difflib.get_close_matches(name, known, n=1, cutoff=0.6) + close = difflib.get_close_matches(name, sorted(known), n=1, cutoff=0.6) hint = f" (Did you mean '{close[0]}'?)" if close else "" parts.append(f"'{name}'{hint}")