Skip to content
Open
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
8 changes: 3 additions & 5 deletions docs/examples/example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,13 @@
"\n",
"Click [here](https://github.com/viamrobotics/viam-python-sdk/blob/main/docs/examples/module_step2.py) to see the file with its new `MODEL` variable, creator function, and `main()` function.\n",
"\n",
"#### Optional Validator and Reconfiguration Functions\n",
"Modules also have support for validator and reconfiguration functions.\n",
"#### Optional Validator Function\n",
"Modules also have support for validator functions.\n",
"\n",
"Custom validation can be done by specifying a validate function. Validate functions can raise errors that will be returned to the parent through gRPC. Validate functions return two sequences of strings: the first represents the implicit required dependencies and the second represents the optional dependencies. If there are none for either, return an empty sequence `[]` (if there are no required dependencies and no optional dependencies, return `[], []`) .\n",
"\n",
"For example, let's say `MySensor` had a `multiplier` argument that is used to multiply the returned readings in `get_readings()`. The validation function can check if a multiplier attribute was provided and prevent erroneous resource start ups.\n",
"\n",
"Reconfiguration specifies what values can change based on a new configuration. To allow this, add a `reconfigure()` function to the `MySensor` class. A good pattern is to call `reconfigure()` within `new()`, since initialization and reconfiguration are usually very similar.\n",
"\n",
"```python\n",
"# ADD `Sequence` and `Tuple` FROM `typing`.\n",
"from typing import Sequence, Tuple\n",
Expand Down Expand Up @@ -348,7 +346,7 @@
" async def main():\n",
" Registry.register_resource_creator(Sensor.SUBTYPE, MySensor.MODEL, ResourceCreatorRegistration(MySensor.new, MySensor.validate_config))\n",
"```\n",
"Click [here](https://github.com/viamrobotics/viam-python-sdk/blob/main/docs/examples/module_step2_optional.py) to see the new `wifi_sensor_module.py` with the optional validator and reconfiguration functions."
"Click [here](https://github.com/viamrobotics/viam-python-sdk/blob/main/docs/examples/module_step2_optional.py) to see the new `wifi_sensor_module.py` with the optional validator function."
]
},
{
Expand Down
3 changes: 1 addition & 2 deletions examples/complex_module/src/base/my_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from viam.components.base import Base
from viam.components.motor import Motor
from viam.module.types import Reconfigurable
from viam.proto.app.robot import ComponentConfig
from viam.resource.base import ResourceBase
from viam.proto.common import Geometry, Vector3, ResourceName
Expand All @@ -13,7 +12,7 @@
from viam.utils import ValueTypes, struct_to_dict


class MyBase(Base, Reconfigurable):
class MyBase(Base):
"""
MyBase implements a base that only supports set_power (basic forward/back/turn controls), is_moving (check if in motion), and stop (stop
all motion).
Expand Down
3 changes: 1 addition & 2 deletions examples/complex_module/src/gizmo/my_gizmo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from viam.logging import getLogger
from viam.utils import ValueTypes
from viam.module.types import Reconfigurable
from viam.proto.app.robot import ComponentConfig
from viam.proto.common import ResourceName
from viam.resource.base import ResourceBase
Expand All @@ -16,7 +15,7 @@
LOGGER = getLogger(__name__)


class MyGizmo(Gizmo, Reconfigurable):
class MyGizmo(Gizmo):
"""This is the specific implementation of a ``Gizmo`` (defined in api.py).

It inherits from Gizmo, as well conforms to the ``Reconfigurable`` protocol, which signifies that this component can be reconfigured.
Expand Down
3 changes: 1 addition & 2 deletions examples/complex_module/src/summation/my_summation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from typing_extensions import Self

from viam.module.types import Reconfigurable
from viam.utils import ValueTypes
from viam.proto.app.robot import ComponentConfig
from viam.proto.common import ResourceName
Expand All @@ -13,7 +12,7 @@
from ..summation.api import SummationService


class MySummationService(SummationService, Reconfigurable):
class MySummationService(SummationService):
"""This is the specific implementation of a ``SummationService`` (defined in api.py)

It inherits from SummationService, as well as conforms to the ``Reconfigurable`` protocol, which signifies that this component can be
Expand Down
3 changes: 1 addition & 2 deletions examples/optionaldepsmodule/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from viam.components.generic import Generic
from viam.components.motor import Motor
from viam.module.types import Reconfigurable
from viam.proto.app.robot import ComponentConfig
from viam.resource.base import ResourceBase
from viam.proto.common import ResourceName
Expand All @@ -15,7 +14,7 @@
import asyncio


class Foo(Generic, Reconfigurable):
class Foo(Generic):
MODEL: ClassVar[Model] = Model(ModelFamily("acme", "demo"), "foo")

def __init__(self, name: str):
Expand Down
22 changes: 9 additions & 13 deletions src/viam/module/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
from ..services.slam import SLAM # noqa: F401
from ..services.vision import Vision # noqa: F401
from .service import ModuleRPCService
from .types import Reconfigurable, Stoppable
from .types import Stoppable

NO_MODULE_PARENT = os.environ.get("VIAM_NO_MODULE_PARENT", "").lower() == "true"

Expand Down Expand Up @@ -233,23 +233,19 @@ async def add_resource(self, request: AddResourceRequest, *, deadline: Optional[
self.server.register(resource)

async def reconfigure_resource(self, request: ReconfigureResourceRequest):
dependencies = await self._get_dependencies(request.dependencies)
config: ComponentConfig = request.config
api = API.from_string(config.api)
name = config.name
rn = ResourceName(namespace=api.namespace, type=api.resource_type, subtype=api.resource_subtype, name=name)
resource = self.server.get_resource(ResourceBase, rn)
if isinstance(resource, Reconfigurable):
resource.reconfigure(config, dependencies)
else:
if isinstance(resource, Stoppable):
if iscoroutinefunction(resource.stop):
await resource.stop()
else:
resource.stop()
add_request = AddResourceRequest(config=request.config, dependencies=request.dependencies)
await self.server.remove_resource(rn)
await self.add_resource(add_request)
if isinstance(resource, Stoppable):
if iscoroutinefunction(resource.stop):
await resource.stop()
else:
resource.stop()
add_request = AddResourceRequest(config=request.config, dependencies=request.dependencies)
await self.server.remove_resource(rn)
await self.add_resource(add_request)

async def remove_resource(self, request: RemoveResourceRequest):
rn = resource_name_from_string(request.name)
Expand Down
9 changes: 3 additions & 6 deletions src/viam/module/types.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
from typing import Any, Mapping, Optional, Protocol, runtime_checkable

from viam.proto.app.robot import ComponentConfig
from viam.proto.common import ResourceName
from viam.resource.base import ResourceBase
import warnings


@runtime_checkable
class Reconfigurable(Protocol):
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove this class completely? Or not, because it would crash at runtime for users who bump the SDK version but forget to remove it from their code?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it for now, but can you add a deprecated warning to it please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean use __init_subclass__ or something so it prints at runtime when something subclasses this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah exactly. Not sure if __init_subclass__ works for protocols. If not, maybe we do just remove the protocl. I think it's been removed in the other SDKs already, correct?

Copy link
Copy Markdown
Contributor Author

@aldenh-viam aldenh-viam May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__init_subclass__ works for me:

$ python3 -W always  # always show warnings
Python 3.13.12 (main, Feb  3 2026, 17:53:27) [GCC 15.2.0] on linux
>>> from viam.module.types import Reconfigurable
>>> class Test(Reconfigurable):
...     pass
...
<frozen abc>:106: DeprecationWarning: Reconfigure is deprecated, and resources will always rebuild. It is not necessary to implement Reconfigurable.
$ git diff src/viam/module/types.py
diff --git a/src/viam/module/types.py b/src/viam/module/types.py
index 4930653426..3ac0e77067 100644
--- a/src/viam/module/types.py
+++ b/src/viam/module/types.py
@@ -1,9 +1,12 @@
 from typing import Any, Mapping, Optional, Protocol, runtime_checkable
+import warnings


 @runtime_checkable
 class Reconfigurable(Protocol):
     """The Reconfigurable protocol defines the requirements for making a resource Reconfigurable"""
+    def __init_subclass__(*args, **kwargs):
+        warnings.warn("Reconfigure is deprecated, and resources will always rebuild. It is not necessary to implement Reconfigurable.", DeprecationWarning, stacklevel=2)

I think it's been removed in the other SDKs already, correct?

Yes, it's completely removed in C++ already, and will be in Go once the main PR gets merged.

"""The Reconfigurable protocol defines the requirements for making a resource Reconfigurable"""

def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]): ...
def __init_subclass__(*args, **kwargs):
warnings.warn("Reconfigure is deprecated, and resources will always rebuild. It is not necessary to implement Reconfigurable.", DeprecationWarning, stacklevel=2)


@runtime_checkable
Expand Down
3 changes: 1 addition & 2 deletions tests/mocks/module/gizmo/my_gizmo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from typing_extensions import Self

from viam.components.component_base import ComponentBase
from viam.module.types import Reconfigurable
from viam.proto.app.robot import ComponentConfig
from viam.proto.common import ResourceName
from viam.resource.base import ResourceBase
Expand All @@ -12,7 +11,7 @@
from ..gizmo.api import Gizmo


class MyGizmo(Gizmo, Reconfigurable):
class MyGizmo(Gizmo):
MODEL: ClassVar[Model] = Model(ModelFamily("acme", "demo"), "mygizmo")
my_arg: str
closed: bool = False
Expand Down
3 changes: 1 addition & 2 deletions tests/mocks/module/summation/my_summation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from typing_extensions import Self

from viam.module.types import Reconfigurable
from viam.proto.app.robot import ComponentConfig
from viam.proto.common import ResourceName
from viam.resource.base import ResourceBase
Expand All @@ -11,7 +10,7 @@
from ..summation.api import SummationService


class MySummationService(SummationService, Reconfigurable):
class MySummationService(SummationService):
MODEL: ClassVar[Model] = Model.from_string("acme:demo:mysum")
subtract: bool

Expand Down
34 changes: 0 additions & 34 deletions tests/test_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
from .mocks.module.gizmo.api import Gizmo
from .mocks.module.gizmo.my_gizmo import MyGizmo
from .mocks.module.summation.api import SummationService
from .mocks.module.summation.my_summation import MySummationService
from .test_robot import service as robot_service # noqa: F401


Expand Down Expand Up @@ -108,39 +107,6 @@ async def test_add_resource(self, module: Module):
await module.add_resource(req)
assert SummationService.get_resource_name("mysum1") in module.server.resources

async def test_reconfigure_resource(self, module: Module):
await self.test_add_resource(module)

gizmo = module.server.get_resource(MyGizmo, Gizmo.get_resource_name("gizmo1"))
assert gizmo.my_arg == "arg1"
req = ReconfigureResourceRequest(
config=ComponentConfig(
name="gizmo1",
namespace="acme",
type="gizmo",
model="acme:demo:mygizmo",
attributes=dict_to_struct({"arg1": "arg2", "motor": "motor1"}),
api="acme:component:gizmo",
)
)
await module.reconfigure_resource(req)
assert gizmo.my_arg == "arg2"

summer = module.server.get_resource(MySummationService, SummationService.get_resource_name("mysum1"))
assert summer.subtract is False
req = ReconfigureResourceRequest(
config=ComponentConfig(
name="mysum1",
namespace="acme",
type="summation",
model="acme:demo:mysum",
attributes=dict_to_struct({"subtract": True}),
api="acme:service:summation",
)
)
await module.reconfigure_resource(req)
assert summer.subtract is True

async def test_add_resource_with_deps(self, robot_service: RobotService, module: Module): # noqa: F811
async with ChannelFor([robot_service]) as channel:
_ = mock.patch("viam.module.module.Module._connect_to_parent")
Expand Down