Skip to content

Add save_settings parameter to @lt.thing_action decorator for automatic settings persistence #156

@julianstirling

Description

@julianstirling

Problem

Some @lt.thing_action methods may modify Thing Settings indirectly (without calling the setter). These need to call self.save_settings() manually. Currently, there's no clean way to automatically save settings after a thing action completes without breaking dependency injection.

Current Workaround

@lt.thing_action
def set_background(self, portal: lt.deps.BlockingPortal):
    """Action that modifies settings indirectly."""
    # ... action logic that modifies thing_setting data ...
    self.save_settings()  # Manual call required

Attempted Solution That Failed

I tried creating a decorator to handle this automatically:

import functools
def modifies_settings(func):
    """Save settings after function completes, for methods that modify Thing Settings."""
    @functools.wraps(func)
    def action_wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        # args[0] is self as this must be a method of a thing action.
        args[0].save_settings()
        return result
    return action_wrapper

then

@lt.thing_action
@modifies_settings
def set_background(self, portal: lt.deps.BlockingPortal):
    # ... action logic ...

This fails at labthings_fastapi/utilities/introspection.py", line 101, in input_model_from_signature with a number of Pydantic _generate_schema.py" calls

Proposed Solution

Add an optional save_settings parameter to the @thing_action decorator:

@lt.thing_action(save_settings=True)
def set_background(self, portal: lt.deps.BlockingPortal):
    """Action that modifies settings indirectly."""
    # ... action logic that modifies thing_setting data ...
    # save_settings() called automatically after successful completion

This would be particularly useful for any action that indirectly modifies @thing_setting properties through complex logic. But would be cleaner than relying on save_settings() being called manually, without changing any existing behaviour.

Full log from the decorator attempt

LabThings Could't Load

Something went wrong when setting up your LabThings server.

Please check your configuration and try again.

More details may be shown below:

Unable to generate pydantic-core schema for . Set `arbitrary_types_allowed=True` in the model_config to ignore this error or implement `__get_pydantic_core_schema__` on your type to fully support it.

If you got this error by calling handler() within `__get_pydantic_core_schema__` then you likely need to call `handler.generate_schema()` since we do not call `__get_pydantic_core_schema__` on `` otherwise to avoid infinite recursion.

For further information visit https://errors.pydantic.dev/2.10/u/schema-for-unknown-type

The following Things loaded successfully:

Your configuration:

{
  "things": {
    "/camera/": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera",
    "/stage/": "openflexure_microscope_server.things.stage.dummy:DummyStage",
    "/auto_recentre_stage/": "openflexure_microscope_server.things.auto_recentre_stage:RecentringThing",
    "/autofocus/": "openflexure_microscope_server.things.autofocus:AutofocusThing",
    "/camera_stage_mapping/": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
    "/system/": "openflexure_microscope_server.things.system:OpenFlexureSystem",
    "/smart_scan/": {
      "class": "openflexure_microscope_server.things.smart_scan:SmartScanThing",
      "kwargs": {
        "scans_folder": "./openflexure/scans/"
      }
    }
  },
  "settings_folder": "./openflexure/settings/",
  "log_folder": "./openflexure/logs/"
}

Traceback

Traceback (most recent call last):
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/src/openflexure_microscope_server/server/__init__.py", line 85, in serve_from_cli
    server = lt.cli.server_from_config(config)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/labthings_fastapi/server/__init__.py", line 172, in server_from_config
    cls = object_reference_to_object(thing["class"])
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/labthings_fastapi/utilities/object_reference_to_object.py", line 15, in object_reference_to_object
    obj = importlib.import_module(modname)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "", line 1204, in _gcd_import
  File "", line 1176, in _find_and_load
  File "", line 1126, in _find_and_load_unlocked
  File "", line 241, in _call_with_frames_removed
  File "", line 1204, in _gcd_import
  File "", line 1176, in _find_and_load
  File "", line 1147, in _find_and_load_unlocked
  File "", line 690, in _load_unlocked
  File "", line 940, in exec_module
  File "", line 241, in _call_with_frames_removed
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/src/openflexure_microscope_server/things/camera/__init__.py", line 157, in 
    class BaseCamera(lt.Thing):
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/src/openflexure_microscope_server/things/camera/__init__.py", line 540, in BaseCamera
    @lt.thing_action
     ^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/labthings_fastapi/decorators/__init__.py", line 70, in thing_action
    return mark_thing_action(func, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/labthings_fastapi/decorators/__init__.py", line 57, in mark_thing_action
    return ActionDescriptorSubclass(func, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/labthings_fastapi/descriptors/action.py", line 70, in __init__
    self.input_model = input_model_from_signature(
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/labthings_fastapi/utilities/introspection.py", line 101, in input_model_from_signature
    model = create_model(  # type: ignore[call-overload]
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/main.py", line 1678, in create_model
    return meta(
           ^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_model_construction.py", line 224, in __new__
    complete_model_class(
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_model_construction.py", line 602, in complete_model_class
    schema = cls.__get_pydantic_core_schema__(cls, handler)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/main.py", line 702, in __get_pydantic_core_schema__
    return handler(source)
           ^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py", line 84, in __call__
    schema = self._handler(source_type)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 610, in generate_schema
    schema = self._generate_schema_inner(obj)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 879, in _generate_schema_inner
    return self._model_schema(obj)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 691, in _model_schema
    {k: self._generate_md_field_schema(k, v, decorators) for k, v in fields.items()},
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 691, in 
    {k: self._generate_md_field_schema(k, v, decorators) for k, v in fields.items()},
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 1071, in _generate_md_field_schema
    common_field = self._common_field_schema(name, field_info, decorators)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 1263, in _common_field_schema
    schema = self._apply_annotations(
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 2056, in _apply_annotations
    schema = get_inner_schema(source_type)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py", line 84, in __call__
    schema = self._handler(source_type)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 2131, in new_handler
    schema = metadata_get_schema(source, get_inner_schema)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 2127, in 
    lambda source, handler: handler(source)
                            ^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_schema_generation_shared.py", line 84, in __call__
    schema = self._handler(source_type)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 2037, in inner_handler
    schema = self._generate_schema_inner(obj)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 884, in _generate_schema_inner
    return self.match_type(obj)
           ^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 995, in match_type
    return self._unknown_type_schema(obj)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/js3214/Documents/gitstuffs/openflexure-microscope-server/.venv/lib/python3.11/site-packages/pydantic/_internal/_generate_schema.py", line 513, in _unknown_type_schema
    raise PydanticSchemaGenerationError(
pydantic.errors.PydanticSchemaGenerationError: Unable to generate pydantic-core schema for . Set `arbitrary_types_allowed=True` in the model_config to ignore this error or implement `__get_pydantic_core_schema__` on your type to fully support it.

If you got this error by calling handler() within `__get_pydantic_core_schema__` then you likely need to call `handler.generate_schema()` since we do not call `__get_pydantic_core_schema__` on `` otherwise to avoid infinite recursion.

For further information visit https://errors.pydantic.dev/2.10/u/schema-for-unknown-type

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions