1818import datetime
1919import logging
2020from collections import deque
21- from functools import partial , wraps
21+ from functools import partial
2222import inspect
2323from threading import Thread , Lock
2424import uuid
2727 Annotated ,
2828 Any ,
2929 Callable ,
30+ Concatenate ,
31+ Generic ,
3032 Optional ,
31- Literal ,
32- Union ,
33+ ParamSpec ,
34+ TypeVar ,
3335 overload ,
3436)
3537from weakref import WeakSet
3638import weakref
3739from fastapi import FastAPI , HTTPException , Request , Body , BackgroundTasks
3840from pydantic import BaseModel , create_model
3941
40- from labthings_fastapi . logs import add_thing_log_destination
41-
42+ from . base_descriptor import BaseDescriptor
43+ from . logs import add_thing_log_destination
4244from .utilities import model_to_dict
43- from .thing_description ._model import LinkElement
4445from .invocations import InvocationModel , InvocationStatus , LogRecordModel
4546from .dependencies .invocation import NonWarningInvocationID
4647from .exceptions import (
5556 EmptyInput ,
5657 StrictEmptyInput ,
5758 fastapi_dependency_params ,
58- get_docstring ,
59- get_summary ,
6059 input_model_from_signature ,
6160 return_type ,
6261)
6362from .thing_description import type_to_dataschema
64- from .thing_description ._model import ActionAffordance , ActionOp , Form
63+ from .thing_description ._model import ActionAffordance , ActionOp , Form , LinkElement
6564from .utilities import labthings_data
6665
6766
@@ -622,7 +621,15 @@ def delete_invocation(id: uuid.UUID) -> None:
622621"""
623622
624623
625- class ActionDescriptor :
624+ ActionParams = ParamSpec ("ActionParams" )
625+ ActionReturn = TypeVar ("ActionReturn" )
626+ OwnerT = TypeVar ("OwnerT" , bound = "Thing" )
627+
628+
629+ class ActionDescriptor (
630+ BaseDescriptor [Callable [ActionParams , ActionReturn ]],
631+ Generic [ActionParams , ActionReturn , OwnerT ],
632+ ):
626633 """Wrap actions to enable them to be run over HTTP.
627634
628635 This class is responsible for generating the action description for
@@ -640,7 +647,7 @@ class ActionDescriptor:
640647
641648 def __init__ (
642649 self ,
643- func : Callable ,
650+ func : Callable [ Concatenate [ OwnerT , ActionParams ], ActionReturn ] ,
644651 response_timeout : float = 1 ,
645652 retention_time : float = 300 ,
646653 ) -> None :
@@ -662,7 +669,12 @@ def __init__(
662669 :param retention_time: how long, in seconds, the action should be kept
663670 for after it has completed.
664671 """
672+ super ().__init__ ()
665673 self .func = func
674+ if func .__doc__ is not None :
675+ # Use the docstring from the function, if there is one.
676+ self .__doc__ = func .__doc__
677+ name = func .__name__ # this is checked in __set_name__
666678 self .response_timeout = response_timeout
667679 self .retention_time = retention_time
668680 self .dependency_params = fastapi_dependency_params (func )
@@ -673,63 +685,47 @@ def __init__(
673685 )
674686 self .output_model = return_type (func )
675687 self .invocation_model = create_model (
676- f"{ self . name } _invocation" ,
688+ f"{ name } _invocation" ,
677689 __base__ = InvocationModel ,
678690 input = (self .input_model , ...),
679691 output = (Optional [self .output_model ], None ),
680692 )
681- self .invocation_model .__name__ = f"{ self . name } _invocation"
693+ self .invocation_model .__name__ = f"{ name } _invocation"
682694
683- @overload
684- def __get__ (self , obj : Literal [None ], type : type [Thing ]) -> ActionDescriptor : # noqa: D105
685- ...
695+ def __set_name__ (self , owner : type [Thing ], name : str ) -> None :
696+ """Ensure the action name matches the function name.
686697
687- @overload
688- def __get__ (self , obj : Thing , type : type [Thing ] | None = None ) -> Callable : # noqa: D105
689- ...
698+ It's assumed in a few places that the function name and the
699+ descriptor's name are the same. This should always be the case
700+ if it's used as a decorator.
701+
702+ :param owner: The class owning the descriptor.
703+ :param name: The name of the descriptor in the class.
704+ :raises ValueError: if the action name does not match the function name.
705+ """
706+ super ().__set_name__ (owner , name )
707+ if self .name != self .func .__name__ :
708+ raise ValueError (
709+ f"Action name '{ self .name } ' does not match function name "
710+ f"'{ self .func .__name__ } '" ,
711+ )
690712
691- def __get__ (
692- self , obj : Optional [Thing ], type : Optional [type [Thing ]] = None
693- ) -> Union [ActionDescriptor , Callable ]:
713+ def instance_get (self , obj : Thing ) -> Callable [ActionParams , ActionReturn ]:
694714 """Return the function, bound to an object as for a normal method.
695715
696716 This currently doesn't validate the arguments, though it may do so
697717 in future. In its present form, this is equivalent to a regular
698718 Python method, i.e. all we do is supply the first argument, `self`.
699719
700- If `obj` is None, the descriptor is returned, so we can get
701- the descriptor conveniently as an attribute of the class.
702-
703720 :param obj: the `.Thing` to which we are attached. This will be
704721 the first argument supplied to the function wrapped by this
705722 descriptor.
706- :param type: the class of the `.Thing` to which we are attached.
707- If the descriptor is accessed via the class it is returned
708- directly.
709- :return: the action function, bound to ``obj`` (when accessed
710- via an instance), or the descriptor (accessed via the class).
723+ :return: the action function, bound to ``obj``.
711724 """
712- if obj is None :
713- return self
714- # TODO: do we attempt dependency injection here? I think not.
715- # If we want dependency injection, we should be calling the action
716- # via some sort of client object.
717- return partial (self .func , obj )
718-
719- @property
720- def name (self ) -> str :
721- """The name of the wrapped function."""
722- return self .func .__name__
723-
724- @property
725- def title (self ) -> str :
726- """A human-readable title."""
727- return get_summary (self .func ) or self .name
728-
729- @property
730- def description (self ) -> str | None :
731- """A description of the action."""
732- return get_docstring (self .func , remove_summary = True )
725+ # `obj` should be of type `OwnerT`, but `BaseDescriptor` currently
726+ # isn't generic in the type of the owning Thing, so we can't express
727+ # that here.
728+ return partial (self .func , obj ) # type: ignore[arg-type]
733729
734730 def _observers_set (self , obj : Thing ) -> WeakSet :
735731 """Return a set used to notify changes.
@@ -926,50 +922,33 @@ def action_affordance(
926922 )
927923
928924
929- def mark_action (func : Callable , ** kwargs : Any ) -> ActionDescriptor :
930- r"""Mark a method of a Thing as an Action.
931-
932- We replace the function with a descriptor that's a
933- subclass of `.ActionDescriptor`
934-
935- :param func: The function to be decorated.
936- :param \**kwargs: Additional keyword arguments are passed to the constructor
937- of `.ActionDescriptor`.
938-
939- :return: An `.ActionDescriptor` wrapping the method.
940- """
941-
942- class ActionDescriptorSubclass (ActionDescriptor ):
943- pass
944-
945- return ActionDescriptorSubclass (func , ** kwargs )
946-
947-
948925@overload
949- def action (func : Callable , ** kwargs : Any ) -> ActionDescriptor : ...
926+ def action (
927+ func : Callable [Concatenate [OwnerT , ActionParams ], ActionReturn ], ** kwargs : Any
928+ ) -> ActionDescriptor [ActionParams , ActionReturn , OwnerT ]: ...
950929
951930
952931@overload
953932def action (
954933 ** kwargs : Any ,
955934) -> Callable [
956935 [
957- Callable ,
936+ Callable [ Concatenate [ OwnerT , ActionParams ], ActionReturn ] ,
958937 ],
959- ActionDescriptor ,
938+ ActionDescriptor [ ActionParams , ActionReturn , OwnerT ] ,
960939]: ...
961940
962941
963- @wraps (mark_action )
964942def action (
965- func : Optional [Callable ] = None , ** kwargs : Any
943+ func : Callable [Concatenate [OwnerT , ActionParams ], ActionReturn ] | None = None ,
944+ ** kwargs : Any ,
966945) -> (
967- ActionDescriptor
946+ ActionDescriptor [ ActionParams , ActionReturn , OwnerT ]
968947 | Callable [
969948 [
970- Callable ,
949+ Callable [ Concatenate [ OwnerT , ActionParams ], ActionReturn ] ,
971950 ],
972- ActionDescriptor ,
951+ ActionDescriptor [ ActionParams , ActionReturn , OwnerT ] ,
973952 ]
974953):
975954 r"""Mark a method of a `.Thing` as a LabThings Action.
@@ -996,6 +975,6 @@ def action(
996975 # return a partial object, which then calls the
997976 # wrapped function once.
998977 if func is not None :
999- return mark_action (func , ** kwargs )
978+ return ActionDescriptor (func , ** kwargs )
1000979 else :
1001- return partial (mark_action , ** kwargs )
980+ return partial (ActionDescriptor , ** kwargs )
0 commit comments