1- """
2- The `Thing` class enables most of the functionality of this library,
3- and is the way in to most of its features. In the future, we might
4- support a stub version of the class in a separate package, so
5- that instrument control libraries can be LabThings compatible
6- without a hard dependency on LabThings. But that is something we
7- will do in the future...
1+ """A class to represent hardware or software Things.
2+
3+ The `.Thing` class enables most of the functionality of this library,
4+ and is the way in to most of its features. See wot_cc_ and labthings_cc_
5+ for more.
86"""
97
108from __future__ import annotations
11- from typing import TYPE_CHECKING , Optional
9+ from typing import TYPE_CHECKING , Any , Optional , Self
1210from collections .abc import Mapping
1311import logging
1412import os
@@ -44,67 +42,85 @@ class Thing:
4442 a particular function - it will correspond to a path on the server, and a Thing
4543 Description document.
4644
47- ## Subclassing Notes
45+ Subclassing Notes
46+ -----------------
4847
49- * `__init__`: You should accept any arguments you need to configure the Thing
50- in `__init__`. Don't initialise any hardware at this time, as your Thing may
48+ * `` __init__` `: You should accept any arguments you need to configure the Thing
49+ in `` __init__` `. Don't initialise any hardware at this time, as your Thing may
5150 be instantiated quite early, or even at import time.
52- * `__enter__(self)` and `__exit__(self, exc_t, exc_v, exc_tb)` are where you
51+ * `` __enter__(self)`` and `` __exit__(self, exc_t, exc_v, exc_tb)` ` are where you
5352 should start and stop communications with the hardware. This is Python's standard
54- "context manager" protocol. The arguments of `__exit__` will be `None` unless
53+ "context manager" protocol. The arguments of `` __exit__`` will be `` None` ` unless
5554 an exception has occurred. You should be safe to ignore them, and just include
56- code that will close down your hardware. It's equivalent to a `finally:` block.
57- * Properties and Actions are defined using decorators: the `@thing_action` decorator
58- declares a method to be an action, which will run when it's triggered, and the
59- `@thing_property` decorator (or `ThingProperty` descriptor) does the same for
60- a property. See the documentation on those functions for more detail.
55+ code that will close down your hardware. It's equivalent to a ``finally`:` block.
56+ * Properties and Actions are defined using decorators: the :deco:`.thing_action`
57+ decorator declares a method to be an action, which will run when it's triggered,
58+ and the :deco:`.thing_property` decorator (or `.ThingProperty` descriptor) does
59+ the same for a property. See the documentation on those functions for more
60+ detail.
6161 * `title` will be used in various places as the human-readable name of your Thing,
6262 so it makes sense to set this in a subclass.
6363
64- There are various LabThings methods that you should avoid overriding unless you know
65- what you are doing: anything not mentioned above that's defined in `Thing` is
66- probably best left along . They may in time be collected together into a single
64+ There are various LabThings methods that you should avoid overriding unless you
65+ know what you are doing: anything not mentioned above that's defined in `Thing` is
66+ probably best left alone . They may in time be collected together into a single
6767 object to avoid namespace clashes.
6868 """
6969
7070 title : str
71+ """A human-readable description of the Thing"""
7172 _labthings_blocking_portal : Optional [BlockingPortal ] = None
73+ """See concurrency_ for why blocking portal is needed."""
7274 path : Optional [str ]
75+ """The path at which the `.Thing` is exposed over HTTP."""
7376
74- async def __aenter__ (self ):
77+ async def __aenter__ (self ) -> Self :
7578 """Context management is used to set up/close the thing.
7679
7780 As things (currently) do everything with threaded code, we define
78- async __aenter__ and __aexit__ wrappers to call the synchronous
81+ async `` __aenter__`` and `` __aexit__`` wrappers to call the synchronous
7982 code, if it exists.
83+
84+ :return: this object.
8085 """
8186 if hasattr (self , "__enter__" ):
8287 return await run_sync (self .__enter__ )
8388 else :
8489 return self
8590
86- async def __aexit__ (self , exc_t , exc_v , exc_tb ):
91+ async def __aexit__ (
92+ self , exc_t : type [Exception ] | None , exc_v : Exception | None , exc_tb : Any
93+ ) -> None :
8794 """Wrap context management functions, if they exist.
8895
89- See __aenter__ docs for more details.
96+ See ``__aenter__`` for more details.
97+
98+ :param exc_t: The type of the exception, or ``None``.
99+ :param exc_v: The exception that occurred, or ``None``.
100+ :param exc_tb: The traceback for the exception, or ``None``.
90101 """
91102 if hasattr (self , "__exit__" ):
92- return await run_sync (self .__exit__ , exc_t , exc_v , exc_tb )
103+ await run_sync (self .__exit__ , exc_t , exc_v , exc_tb )
93104
94105 def attach_to_server (
95106 self , server : ThingServer , path : str , setting_storage_path : str
96- ):
97- """Attatch this thing to the server.
107+ ) -> None :
108+ """Attach this thing to the server.
98109
99110 Things need to be attached to a server before use to function correctly.
100111
101- :param server: The server to attach this Thing to
102- :param settings_storage_path: The path on disk to save the any Thing Settings
112+ :param server: The server to attach this Thing to.
113+ :param path: The root URL for the Thing.
114+ :param setting_storage_path: The path on disk to save the any Thing Settings
103115 to. This should be the path to a json file. If it does not exist it will be
104116 created.
105117
106- Wc3 Web Of Things explanation:
107- This will add HTTP handlers to an app for all Interaction Affordances
118+ Attaching the `.Thing` to a `.ThingServer` allows the `.Thing` to start
119+ actions, load its settings from the correct place, and create HTTP endpoints
120+ to allow it to be accessed from the HTTP API.
121+
122+ We create HTTP endpoints for all wot_affordances_ on the `.Thing`, as well
123+ as any `.EndpointDescriptor` descriptors.
108124 """
109125 self .path = path
110126 self .action_manager : ActionManager = server .action_manager
@@ -139,7 +155,7 @@ async def websocket(ws: WebSocket):
139155
140156 @property
141157 def _settings (self ) -> Optional [dict [str , ThingSetting ]]:
142- """A private property that returns a dict of all settings for this Thing
158+ """A private property that returns a dict of all settings for this Thing.
143159
144160 Each dict key is the name of the setting, the corresponding value is the
145161 ThingSetting class (a descriptor). This can be used to directly get the
@@ -159,11 +175,31 @@ def _settings(self) -> Optional[dict[str, ThingSetting]]:
159175
160176 @property
161177 def setting_storage_path (self ) -> Optional [str ]:
162- """The storage path for settings. This is set as the Thing is added to a server"""
178+ """The storage path for settings.
179+
180+ .. note::
181+
182+ This is set in `.Thing.attach_to_server`. It is ``None`` during the
183+ ``__init__`` method, so it is best to avoid using settings until the
184+ `.Thing` is set up in ``__enter__``.
185+ """
163186 return self ._setting_storage_path
164187
165- def load_settings (self , setting_storage_path ):
166- """Load settings from json. This is run when the Thing is added to a server"""
188+ def load_settings (self , setting_storage_path : str ) -> None :
189+ """Load settings from json.
190+
191+ Read the JSON file and use it to populate settings.
192+
193+ .. note::
194+ Settings are loaded when the Thing is added to a server, so they will
195+ not be available while the ``__init__`` method is run.
196+
197+ Note that no notifications will be triggered when the settings are set,
198+ so if action is needed (e.g. updating hardware with the loaded settings)
199+ it should be taken in ``__enter__``.
200+
201+ :param setting_storage_path: The path where the settings should be stored.
202+ """
167203 # Ensure that the settings path isn't set during loading or saving will be triggered
168204 self ._setting_storage_path = None
169205 thing_name = type (self ).__name__
@@ -185,7 +221,11 @@ def load_settings(self, setting_storage_path):
185221 self ._setting_storage_path = setting_storage_path
186222
187223 def save_settings (self ):
188- """Save settings to JSON. This is called whenever a setting is updated"""
224+ """Save settings to JSON.
225+
226+ This is called whenever a setting is updated. All settings are written to
227+ the settings file every time.
228+ """
189229 if self ._settings is not None :
190230 setting_dict = {}
191231 for name in self ._settings .keys ():
@@ -202,7 +242,7 @@ def save_settings(self):
202242
203243 @property
204244 def thing_state (self ) -> Mapping :
205- """Return a dictionary summarising our current state
245+ """Return a dictionary summarising our current state.
206246
207247 This is intended to be an easy way to collect metadata from a Thing that
208248 summarises its state. It might be used, for example, to record metadata
@@ -219,8 +259,8 @@ def thing_state(self) -> Mapping:
219259 self ._labthings_thing_state = {}
220260 return self ._labthings_thing_state
221261
222- def validate_thing_description (self ):
223- """Raise an exception if the thing description is not valid"""
262+ def validate_thing_description (self ) -> None :
263+ """Raise an exception if the thing description is not valid. """
224264 td = self .thing_description_dict ()
225265 return validation .validate_thing_description (td )
226266
@@ -231,12 +271,17 @@ def validate_thing_description(self):
231271 def thing_description (
232272 self , path : Optional [str ] = None , base : Optional [str ] = None
233273 ) -> ThingDescription :
234- """A w3c Thing Description representing this thing
274+ """Generate a w3c Thing Description representing this thing.
235275
236276 The w3c Web of Things working group defined a standard representation
237277 of a Thing, which provides a high-level description of the actions,
238278 properties, and events that it exposes. This endpoint delivers a JSON
239- representation of the Thing Description for this Thing.
279+ representation of the wot_td_ for this Thing.
280+
281+ :param path: the URL pointing to this Thing.
282+ :param base: the base URL for all URLs in the thing description.
283+
284+ :return: a Thing Description.
240285 """
241286 path = path or getattr (self , "path" , "{base_uri}" )
242287 if (
@@ -270,26 +315,41 @@ def thing_description_dict(
270315 path : Optional [str ] = None ,
271316 base : Optional [str ] = None ,
272317 ) -> dict :
273- """A w3c Thing Description representing this thing, as a simple dict
318+ r """Describe this Thing with a Thing Description as a simple dict.
274319
275- The w3c Web of Things working group defined a standard representation
276- of a Thing, which provides a high-level description of the actions,
277- properties, and events that it exposes. This endpoint delivers a JSON
278- representation of the Thing Description for this Thing.
320+ See `.Thing.thing_description`\ . This function converts the
321+ return value of that function into a simple dictionary.
322+
323+ :param path: the URL pointing to this Thing.
324+ :param base: the base URL for all URLs in the thing description.
325+
326+ :return: a Thing Description.
279327 """
280328 td : ThingDescription = self .thing_description (path = path , base = base )
281329 td_dict : dict = td .model_dump (exclude_none = True , by_alias = True )
282330 return jsonable_encoder (td_dict )
283331
284- def observe_property (self , property_name : str , stream : ObjectSendStream ):
285- """Register a stream to receive property change notifications"""
332+ def observe_property (self , property_name : str , stream : ObjectSendStream ) -> None :
333+ """Register a stream to receive property change notifications.
334+
335+ :param property_name: the property to register for.
336+ :param stream: the stream used to send events.
337+
338+ :raises KeyError: if the requested name is not defined on this Thing.
339+ """
286340 prop = getattr (self .__class__ , property_name )
287341 if not isinstance (prop , ThingProperty ):
288342 raise KeyError (f"{ property_name } is not a LabThings Property" )
289343 prop ._observers_set (self ).add (stream )
290344
291345 def observe_action (self , action_name : str , stream : ObjectSendStream ):
292- """Register a stream to receive action status change notifications"""
346+ """Register a stream to receive action status change notifications.
347+
348+ :param action_name: the action to register for.
349+ :param stream: the stream used to send events.
350+
351+ :raises KeyError: if the requested name is not defined on this Thing.
352+ """
293353 action = getattr (self .__class__ , action_name )
294354 if not isinstance (action , ActionDescriptor ):
295355 raise KeyError (f"{ action_name } is not an LabThings Action" )
0 commit comments