11"""A mock client that uses a Thing directly.
22
3- Currently this is not a subclass of ThingClient, that may need to change.
4- It's a good idea to create a DirectThingClient at module level, so that type
5- hints work.
3+ When `.Thing` objects interact on the server, it can be very useful to
4+ use an interface that is identical to the `.ThingClient` used to access
5+ the same `.Thing` remotely. This means that code can run either on the
6+ server or on a client, e.g. in a Jupyter notebook where it is much
7+ easier to debug. See things_from_things_ for more detail.
8+
9+ Currently `.DirectThingClient` is not a subclass of `.ThingClient`,
10+ that may need to change. It's a good idea to create a
11+ `.DirectThingClient` at module level, so that type hints work.
612
7- This module may get moved in the near future.
813
914"""
1015
2429from fastapi import Request
2530
2631
32+ __all__ = ["DirectThingClient" , "direct_thing_client_class" ]
33+
34+
2735class DirectThingClient :
36+ """A wrapper for `.Thing` that is a work-a-like for `.ThingClient`.
37+
38+ This class is used to create a class that works like `.ThingClient`
39+ but does not communicate over HTTP. Instead, it wraps a `.Thing` object
40+ and calls its methods directly.
41+
42+ It is not yet 100% identical to `.ThingClient`, in particular `.ThingClient`
43+ returns a lot of data directly as deserialised from JSON, while this class
44+ generally returns `pydantic.BaseModel` instances, without serialisation.
45+
46+ `.DirectThingClient` is generally not used on its own, but is subclassed
47+ (often dynamically) to add the actions and properties of a particular
48+ `.Thing`.
49+ """
50+
2851 __globals__ = globals () # "bake in" globals so dependency injection works
2952 thing_class : type [Thing ]
53+ """The class of the underlying `.Thing` we are wrapping."""
3054 thing_path : str
55+ """The path to the Thing on the server. Relative to the server's base URL."""
3156
3257 def __init__ (self , request : Request , ** dependencies : Mapping [str , Any ]):
33- """Wrapper for a Thing that makes it work like a ThingClient
58+ """Wrapper for a `.Thing` that makes it work like a `.ThingClient`.
59+
60+ This class is designed to be used as a FastAPI dependency, and will
61+ retrieve a `.Thing` based on its ``thing_path`` attribute.
62+ Finding the Thing by class may also be an option in the future.
3463
35- This class is designed to be used as a FastAPI dependency, and will retrieve a
36- Thing based on its `thing_path` attribute. Finding the Thing by class may also
37- be an option in the future.
64+ :param request: This is a FastAPI dependency to access the
65+ `fastapi.Request` object, allowing access to various resources.
66+ :param **dependencies**: Further arguments will be added
67+ dynamically by subclasses, by duplicating this method and
68+ manipulating its signature. Adding arguments with annotated
69+ type hints instructs FastAPI to inject dependency arguments,
70+ such as access to other `.Things`.
3871 """
3972 server = find_thing_server (request .app )
4073 self ._wrapped_thing = server .things [self .thing_path ]
@@ -50,10 +83,28 @@ def property_descriptor(
5083 writeable : bool = True ,
5184 property_path : Optional [str ] = None ,
5285) -> PropertyClientDescriptor :
53- """Create a correctly-typed descriptor that gets and/or sets a property
86+ """Create a correctly-typed descriptor that gets and/or sets a property.
5487
55- This is copy-pasted from labthings_fastapi.client.__init__.property_descriptor
56- TODO: refactor this into a shared function.
88+ .. todo::
89+ This is copy-pasted from labthings_fastapi.client.__init__.property_descriptor
90+ TODO: refactor this into a shared function.
91+
92+ Create a descriptor object that wraps a property. This is for use on
93+ a `.DirectThingClient` subclass.
94+
95+ :param property_name: should be the name of the property (i.e. the
96+ name it takes in the thing description, and also the name it is
97+ assigned to in the class).
98+ :param model: the Python ``type`` or a ``pydantic.BaseModel`` that
99+ represents the datatype of the property.
100+ :param description: text to use for a docstring.
101+ :param readable: whether the property may be read (i.e. has ``__get__``).
102+ :param writeable: whether the property may be written to.
103+ :param property_path: the URL of the ``getproperty`` and ``setproperty``
104+ HTTP endpoints. Currently these must both be the same. These are
105+ relative to the ``base_url``, i.e. the URL of the Thing Description.
106+
107+ :return: a descriptor allowing access to the specified property.
57108 """
58109
59110 class P (PropertyClientDescriptor ):
@@ -90,9 +141,24 @@ def add_action(
90141 name : str ,
91142 action : ActionDescriptor ,
92143) -> None :
93- """Generates an action method and adds it to an attrs dict
144+ """Generate an action method and adds it to an attrs dict.
94145
95146 FastAPI Dependencies are appended to the `dependencies` list.
147+ This list should later be converted to type hints on the class
148+ initialiser, so that FastAPI supplies the dependencies when
149+ the `.DirectThingClient` is initialised.
150+
151+ :param attrs: the attributes of a soon-to-be-created `.DirectThingClient`
152+ subclass. This will be passed to `type()` to create the subclass.
153+ We will add the action method to this dictionary.
154+ :param dependencies: lists the dependency parameters that will be
155+ injected by FastAPI as arguments to the class ``__init__``.
156+ Any dependency parameters of the supplied ``action`` should be
157+ added to this list.
158+ :param name: the name of the action. Should be the name of the
159+ attribute, i.e. we will set ``attrs[name]``, and also match
160+ the ``name`` in the supplied action descriptor.
161+ :param action: an `.ActionDescriptor` to be wrapped.
96162 """
97163
98164 @wraps (action .func )
@@ -125,7 +191,15 @@ def action_method(self, **kwargs):
125191def add_property (
126192 attrs : dict [str , Any ], property_name : str , property : ThingProperty
127193) -> None :
128- """Add a property to a DirectThingClient subclass"""
194+ """Add a property to a DirectThingClient subclass.
195+
196+ :param attrs: the attributes of a soon-to-be-created `.DirectThingClient`
197+ subclass. This will be passed to `type()` to create the subclass.
198+ We will add the property to this dictionary.
199+ :param name: the name of the property. Should be the name of the
200+ attribute, i.e. we will set ``attrs[name]``.
201+ :param property: a `.PropertyDescriptor` to be wrapped.
202+ """
129203 attrs [property_name ] = property_descriptor (
130204 property_name ,
131205 property .model ,
@@ -139,10 +213,23 @@ def direct_thing_client_class(
139213 thing_class : type [Thing ],
140214 thing_path : str ,
141215 actions : Optional [list [str ]] = None ,
142- ):
143- """Create a DirectThingClient from a Thing class and a path
216+ ) -> type [ DirectThingClient ] :
217+ """Create a DirectThingClient from a Thing class and a path.
144218
145219 This is a class, not an instance: it's designed to be a FastAPI dependency.
220+
221+ :param thing_class: The `.Thing` subclass that will be wrapped.
222+ :param thing_path: The path where the `.Thing` is found on the server.
223+ :param actions: An optional list giving a subset of actions that will be
224+ accessed. If this is specified, it may reduce the number of FastAPI
225+ dependencies we need.
226+
227+ :return: a subclass of `DirectThingClient` with attributes that match the
228+ properties and actions of ``thing_class``. The ``__init__` method
229+ will have annotations that instruct FastAPI to supply all the
230+ dependencies needed by its actions.
231+
232+ This class may be used as a FastAPI dependency: see things_from_things_.
146233 """
147234
148235 def init_proxy (self , request : Request , ** dependencies : Mapping [str , Any ]):
0 commit comments