Skip to content

Commit 908f9de

Browse files
committed
Docstring improvements in DirectThingClient
I've added an explicit reference for "using things from other things". This may well want to be its own page in the future.
1 parent 0a23f23 commit 908f9de

File tree

2 files changed

+104
-15
lines changed

2 files changed

+104
-15
lines changed

docs/source/using_things.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Dynamic class generation
1919

2020
The object returned by :meth:`.ThingClient.from_url` is an instance of a dynamically-created subclass of :class:`.ThingClient`. Dynamically creating the class is needed because we don't know what the methods and properties should be until we have downloaded the Thing Description. However, this means most code autocompletion tools, type checkers, and linters will not work well with these classes. In the future, LabThings-FastAPI will generate custom client subclasses that can be shared in client modules, which should fix these problems (see below).
2121

22+
.. _things_from_things:
23+
2224
Using Things from other Things
2325
------------------------------
2426

src/labthings_fastapi/client/in_server.py

Lines changed: 102 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
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

@@ -24,17 +29,45 @@
2429
from fastapi import Request
2530

2631

32+
__all__ = ["DirectThingClient", "direct_thing_client_class"]
33+
34+
2735
class 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):
125191
def 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

Comments
 (0)